Teensy LC, audio library support?

PaulS

Well-known member
But Teensy LC does have the I2S peripheral, and the audio library does have some limited support for Teensy LC. Even fairly simple audio systems quickly use up LC's limited CPU and memory.
Would it be possible to talk to the PT8211 DAC over I2S with a Teensy LC?
This small program compiles without error for Teensy LC but no sine output out of the PT8211 kit. This code does work on a Teensy 3.2.
Code:
// PT8211 bd    Teensy
// VCC          3V3
// GND          GND
// WS           23
// DIN          22
// BCK          9
// --           11 (MCLK)

#include <Audio.h>

uint32_t Freq = 440;

AudioSynthWaveformSine   sine1;
AudioOutputPT8211        pt8211_1;
AudioConnection          patchCord1(sine1, 0, pt8211_1, 0);
AudioConnection          patchCord2(sine1, 0, pt8211_1, 1);

String incoming;

void setup() {
  Serial.begin(115200);
  while (!Serial);                        // wait for serial monitor to be active
  Serial.setTimeout(50);                  // set timeout in millisecs for readString()

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  AudioMemory(12);
  sine1.frequency(Freq);
  sine1.amplitude(1.0);
}

void loop() {
  while (Serial.available()) {
    incoming = Serial.readString();         // read string from serial monitor until timeout
    Serial.print(incoming);                 // return input string to serial monitor
    int32_t value = incoming.toInt();       // convert string into number
    Serial.println(value);                  // return value to serial monitor
    sine1.frequency(value);                 // update frequency
  }
}

Looking into the MKL26Z64 datasheet, the I2S pins are available on the 48pin package:

TLC_I2Cpins.PNG

From the Teensy LC schematic:
pin 33 connects to Teensy pin 15/A1
pin 34 connects to Teensy pin 22/A8
pin 35 connects to Teensy pin 23/A9
pin 36 connects to Teensy pin 9
pin 39 connects to Teensy pin 11


So electrically it seems possible to output I2S on a Teensy LC.
In which audio library files should I now have a look?

[Arduino 1.8.13, Teensyduino 1.53, Windows 10 Pro]

Thanks,
Paul
 
Would it be possible to talk to the PT8211 DAC over I2S with a Teensy LC?

It should be possible. The oversampling code might need to be disabled.


This small program compiles without error for Teensy LC but no sine output out of the PT8211 kit.

Yup. The audio library as ifdefs to put in a do-nothing version of each unsupported output, so the library at least compiles.

To make it actually work, that do-nothing code needs to be replaced with do-something code....


pin 34 connects to Teensy pin 22/A8
pin 35 connects to Teensy pin 23/A9
pin 36 connects to Teensy pin 9
pin 39 connects to Teensy pin 11

These are the same pins Teensy 3.2, 3.5, 3.6 use. When when/if this ever works, the audio shield and pt8211 kit for Teensy 3.x should "just work" when soldered to Teensy LC.



In which audio library files should I now have a look?

Your first place to look is output_dac.cpp. As far as I know, it's currently the only I/O code actually working on Teensy LC.

I'm not 100% happy with the way DMA is used. But it was contributed code and generally seems to work (certainly a lot better than nothing at all), so I merged it. The main problem is Teensy LC's weaker DMA engine doesn't have an interrupt at half-used. So the only interrupt happens when the DMA controller completes the last transfer. If the interrupt isn't serviced within about 22 us (1 audio sample time), the next DMA transfer isn't set up and the output skips a sample.

On Teensy 3.x and 4.x, the DMA controller gives an interrupt at complete and at half-used. The interrupt copies new data into the half of the buffer the DMA controller just finished using. If that interrupt isn't serviced quickly, it's not a problem. The DMA controller keeps streaming audio from the other half of the buffer. As long as the interrupt gets serviced within about 1450 us, it can get the data ready in the other half before it's needed.

The Teensy LC code needlessly retains much of the original structure, using 2 buffers instead of 2 halves of a larger buffer.

The way this should be done on Teensy LC involves using 2 DMA channels, with the channel linking feature, which is controlled by the DCR register of each channel, documented starting on page 378 in the ref manual. Each channel would set the LINKCC bits to 11, and the LCH1 bits to the number of the other channel, as as one channel competes it causes the other to activate. The idea is the same as using the half channel approach on the bigger boards, each each half would be its own DMA channel, using one of the buffers while the other waits to be refilled. Each channel would trigger its own interrupt, which would not need to check the destination address. Each interrupt would refill the buffer for the other channel.

Whether anyone wants to go to all this trouble, I don't know. But I'm taking a little time to write this lengthy explanation in hopes you or maybe Frank would like to dive into the DMA settings and convert the DAC code to using 2 linked channels. Getting 2 channels working, so the DMA interrupt can tolerate more than ~22 us of latency would give a much more solid foundation on Teensy LC.

Of course, if you just want to try getting PT8211 running with the existing 1 DMA channel approach, I can understand that. But please do keep the interrupt latency issue in mind.
 
I've never done much with the LC and don't even know how DMA works on this chip.. :)
But I can try to make at least I2S and the slightly different PT8211 work. If I remember correctly, from years ago, there is only one single bit different. And yes, the oversampling of course. If that works I can try to do more, or use two DMA channels, but the question is, if it worth the time.. having not enough RAM is a problem :)
 
Hi Paul, thanks for your extensive answer!
I'm afraid the DMA stuff is a bit beyond my league.
To be honest, I don't need this to work - I was just curious whether I could built a simple sinegenerator with a Teensy LC when reading about "limited audio support on Teensy LC".

Regards,
Paul
 
Last edited:
Hi Frank, no need to spend your precious time on this adventure. As said above, I was just curious.

Gruesse,
Paul
 
I've never done much with the LC and don't even know how DMA works on this chip.. :)

The good news is the DMA controller is much simpler, with only 4 registers instead of the 8 we normally use in the TCD. There is no "minor loop", just 1 transfer for each trigger event. Fortunately we're configuring the minor loop to do just a single transfer on most of the audio library usage.


but the question is, if it worth the time.. having not enough RAM is a problem :)

I thought this too, but the DAC output does work pretty well.

If you look into this, I would recommend cutting the buffer sizes in half. Then use each DMA channel to consume half of the 2 pending audio blocks. Since it will become 2 separate interrupt functions, only the 2nd one needs to grab the next pending block from the update() function. The first interrupt will always consume the other half of whatever block was available when the 2nd interrupt triggered.

Maybe abandon queuing a second block. It really isn't necessary. Someday I want to go through all the audio output objects and remove the 2nd block pointer.

With those changes, the memory usage for stereo output should be 2 buffers each holding 64 stereo pairs, plus 2 audio blocks filled with data from the rest of the audio design. I believe that adds up to 1024 bytes, plus tiny overhead on the 2 audio blocks. Or if someone routes the same audio to both channels, then only 768 bytes for buffers to make the output work.
 
I'll dig into this. LOL, the ony unsoldered LC I found in my drawer is the original purple Beta board. Cool. It will do it - the right task for a beta ;)
 
@Paul,

i'm afraid it's not that easy.
On the LC CPU, channel-linking "on complete" does not really work. The linked channeld gets triggert only once. (i've tested with manual "software" triggering)
Obviously exactly as name says... only "on complete", one time ..

Then, I tried a different approach:
Here, the second channel is linked via "0x10 Perform a link to channel LCH1 after each cycle-steal transfer"
channel 1 and channel 2 with the same source-address, channel 2 with half of size of channel 1 and with a dummy destination.
Now, i get the "half ready" interrupt (which is "complete" on channel2)
That works.
One time.
Because: it is not possible to use the "circular" feature. It stops as the normal mode, when the transfer is ready. The only difference is, that the source-address gets reset to the start value.
But it stops.
On both channels. Continous restart is not possible. You need an irq to do that.

So, the precious timing is the same as in the output_dac approach.. no win with a 2nd channel.
The interrupt needs to be fast and it needs to reset and restart the DMA.

Friday, I'll continue.. maybe I have an other idea, or I'll just use the simple approach.
 
Last edited:
Hm, or, maybe there is a difference between manual triggering and periphal requests.. maybe it works when triggered by a periphal?
Have to test that... :)
 
Maybe each interrupt will need to reprogram the other channel's settings to get it ready again? At least it's only 4 registers to write.
 
Well isn't this timely... I was starting to mess around with a teensy LC with the intent of generating audio with the Audio Shield. Very interested in your results, Frank!

I have seen other microcontrollers that treat DMA requests from peripherals differently from generic DMA, so that's an idea worth checking out. Often there's support for ping-pong dma chaining... but I haven't gotten into the reference manual for this one yet.

I just got USB serial and MIDI endpoints connected this evening. That uses more code space than I had hoped...
 
Turns out that it will be fun to make this work...

I have not that much time today. Tomorrow..


2021-01-07 12_28_34-Start.png
This means we are fixed to 46.875 or 23.437 kHz, or have to use MCLK as input.
Maybe we can use a timer to trigger the MCLK-Pin...

Edit:
The PT8211 does not use MCLK, so no problem with it.
Don't know if 48MHZ or 24MHz MCLK is an issue for other devices.
 
Last edited:
I ran several tests. Looks like MCLK can only be 16 or 48 MHz.

We need to keep the sample rate at 44117.6, same as Teensy 3.x. The main use case is playing 44.1 kHz WAV files, either from SD card or flash chip. Also from a long-term software maintenance point of view, I want to keep the default sample rate consistent.

Fortunately, it looks like we can get the correct sample rate with MCLK at 48 MHz and I2S_TCR2_DIV = 16 (divide by 34), but with 32 bits per frame rather than the usual 64. Hopefully that is enough to do PT8211 without oversampling?

For I2S, having MCLK / BCLK ratio that isn't a power of 2, and BCLK / LRCLK ratio of 32 is so limiting that it's probably not worthwhile to implement I2S. Almost all chips need MCLK, and most of the ones which don't (MEMS mics) require BCLK / LRCLK ratio 64.

But I2S slave mode should be possible, maybe?


Here's a quick test I cobbled together. It outputs 44.1176 kHz on LRCLK, 1.41 MHz on BCLK, and 48 MHz on MCLK.

Code:
// https://forum.pjrc.com/threads/65735?p=265811&viewfull=1#post265811

void setup() {
  while (!Serial);
  Serial.println("MCLK Test on Teensy LC");
  SIM_SCGC6 |= SIM_SCGC6_I2S;
  I2S0_MCR = I2S_MCR_MICS(0) | I2S_MCR_MOE;
  // I2S0_MDR has no effect on Teensy LC
  CORE_PIN11_CONFIG = PORT_PCR_MUX(6);

  // configure transmitter
  I2S0_TMR = 0;
  I2S0_TCR1 = I2S_TCR1_TFW(0);  // watermark at half fifo size
  I2S0_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP | I2S_TCR2_MSEL(1)
            | I2S_TCR2_BCD | I2S_TCR2_DIV(16);
  I2S0_TCR3 = I2S_TCR3_TCE;
  I2S0_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(15) | I2S_TCR4_MF
            | I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_TCR4_FSD;
  I2S0_TCR5 = I2S_TCR5_WNW(15) | I2S_TCR5_W0W(15) | I2S_TCR5_FBT(15);

  CORE_PIN23_CONFIG = PORT_PCR_MUX(6); // pin 23, PTC2, I2S0_TX_FS (LRCLK)
  CORE_PIN9_CONFIG  = PORT_PCR_MUX(6); // pin  9, PTC3, I2S0_TX_BCLK
  
  I2S0_TCSR = I2S_TCSR_SR;
  I2S0_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;

  Serial.println("Configured");
}

void loop() {
}
 
Code:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <Arduino.h>
#include "output_i2s.h"
#include "memcpy_audio.h"

audio_block_t * AudioOutputI2S::block_left_1st = NULL;
audio_block_t * AudioOutputI2S::block_right_1st = NULL;
audio_block_t * AudioOutputI2S::block_left_2nd = NULL;
audio_block_t * AudioOutputI2S::block_right_2nd = NULL;
uint16_t  AudioOutputI2S::block_left_offset = 0;
uint16_t  AudioOutputI2S::block_right_offset = 0;
bool AudioOutputI2S::update_responsibility = false;
DMAChannel AudioOutputI2S::dma(false);
DMAMEM __attribute__((aligned(32))) static uint32_t i2s_tx_buffer[AUDIO_BLOCK_SAMPLES];

#if defined(__IMXRT1062__)
#include "utility/imxrt_hw.h"
#endif

void AudioOutputI2S::begin(void)
{
	dma.begin(true); // Allocate the DMA channel first

	block_left_1st = NULL;
	block_right_1st = NULL;

	config_i2s();

#if defined(KINETISK)
	CORE_PIN22_CONFIG = PORT_PCR_MUX(6); // pin 22, PTC1, I2S0_TXD0

	dma.TCD->SADDR = i2s_tx_buffer;
	dma.TCD->SOFF = 2;
	dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
	dma.TCD->NBYTES_MLNO = 2;
	dma.TCD->SLAST = -sizeof(i2s_tx_buffer);
	dma.TCD->DADDR = (void *)((uint32_t)&I2S0_TDR0 + 2);
	dma.TCD->DOFF = 0;
	dma.TCD->CITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->DLASTSGA = 0;
	dma.TCD->BITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
	dma.triggerAtHardwareEvent(DMAMUX_SOURCE_I2S0_TX);
	dma.enable();

	I2S0_TCSR = I2S_TCSR_SR;
	I2S0_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;

#elif defined(KINETISL)

	SIM_SCGC6 |= SIM_SCGC6_I2S;
	I2S0_MCR = I2S_MCR_MICS(0) | I2S_MCR_MOE;
	// I2S0_MDR has no effect on Teensy LC
	// CORE_PIN11_CONFIG = PORT_PCR_MUX(6);

	// configure transmitter
	I2S0_TMR = 0;
	I2S0_TCR1 = I2S_TCR1_TFW(0);  // watermark at half fifo size
	I2S0_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP | I2S_TCR2_MSEL(1)
			| I2S_TCR2_BCD | I2S_TCR2_DIV(16);
	I2S0_TCR3 = I2S_TCR3_TCE;
	I2S0_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(15) | I2S_TCR4_MF
			| I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_TCR4_FSD;
	I2S0_TCR5 = I2S_TCR5_WNW(15) | I2S_TCR5_W0W(15) | I2S_TCR5_FBT(15);

	// CORE_PIN23_CONFIG = PORT_PCR_MUX(6); // pin 23, PTC2, I2S0_TX_FS (LRCLK)
	// CORE_PIN9_CONFIG  = PORT_PCR_MUX(6); // pin  9, PTC3, I2S0_TX_BCLK

	I2S0_TCSR = I2S_TCSR_SR;
	I2S0_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;

#elif defined(__IMXRT1062__)
	CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0
	dma.TCD->SADDR = i2s_tx_buffer;
	dma.TCD->SOFF = 2;
	dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
	dma.TCD->NBYTES_MLNO = 2;
	dma.TCD->SLAST = -sizeof(i2s_tx_buffer);
	dma.TCD->DOFF = 0;
	dma.TCD->CITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->DLASTSGA = 0;
	dma.TCD->BITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
	dma.TCD->DADDR = (void *)((uint32_t)&I2S1_TDR0 + 2);
	dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI1_TX);
	dma.enable();

	I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
	I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
#endif
	update_responsibility = update_setup();
	dma.attachInterrupt(isr);
}


void AudioOutputI2S::isr(void)
{
#if defined(KINETISK) || defined(__IMXRT1062__)
	int16_t *dest;
	audio_block_t *blockL, *blockR;
	uint32_t saddr, offsetL, offsetR;

	saddr = (uint32_t)(dma.TCD->SADDR);
	dma.clearInterrupt();
	if (saddr < (uint32_t)i2s_tx_buffer + sizeof(i2s_tx_buffer) / 2) {
		// DMA is transmitting the first half of the buffer
		// so we must fill the second half
		dest = (int16_t *)&i2s_tx_buffer[AUDIO_BLOCK_SAMPLES/2];
		if (AudioOutputI2S::update_responsibility) AudioStream::update_all();
	} else {
		// DMA is transmitting the second half of the buffer
		// so we must fill the first half
		dest = (int16_t *)i2s_tx_buffer;
	}

	blockL = AudioOutputI2S::block_left_1st;
	blockR = AudioOutputI2S::block_right_1st;
	offsetL = AudioOutputI2S::block_left_offset;
	offsetR = AudioOutputI2S::block_right_offset;

	if (blockL && blockR) {
		memcpy_tointerleaveLR(dest, blockL->data + offsetL, blockR->data + offsetR);
		offsetL += AUDIO_BLOCK_SAMPLES / 2;
		offsetR += AUDIO_BLOCK_SAMPLES / 2;
	} else if (blockL) {
		memcpy_tointerleaveL(dest, blockL->data + offsetL);
		offsetL += AUDIO_BLOCK_SAMPLES / 2;
	} else if (blockR) {
		memcpy_tointerleaveR(dest, blockR->data + offsetR);
		offsetR += AUDIO_BLOCK_SAMPLES / 2;
	} else {
		memset(dest,0,AUDIO_BLOCK_SAMPLES * 2);
	}

	arm_dcache_flush_delete(dest, sizeof(i2s_tx_buffer) / 2 );

	if (offsetL < AUDIO_BLOCK_SAMPLES) {
		AudioOutputI2S::block_left_offset = offsetL;
	} else {
		AudioOutputI2S::block_left_offset = 0;
		AudioStream::release(blockL);
		AudioOutputI2S::block_left_1st = AudioOutputI2S::block_left_2nd;
		AudioOutputI2S::block_left_2nd = NULL;
	}
	if (offsetR < AUDIO_BLOCK_SAMPLES) {
		AudioOutputI2S::block_right_offset = offsetR;
	} else {
		AudioOutputI2S::block_right_offset = 0;
		AudioStream::release(blockR);
		AudioOutputI2S::block_right_1st = AudioOutputI2S::block_right_2nd;
		AudioOutputI2S::block_right_2nd = NULL;
	}
#else
	const int16_t *src, *end;
	int16_t *dest;
	audio_block_t *block;
	uint32_t saddr, offset;

	saddr = (uint32_t)(dma.CFG->SAR);
	dma.clearInterrupt();
	if (saddr < (uint32_t)i2s_tx_buffer + sizeof(i2s_tx_buffer) / 2) {
		// DMA is transmitting the first half of the buffer
		// so we must fill the second half
		dest = (int16_t *)&i2s_tx_buffer[AUDIO_BLOCK_SAMPLES/2];
		end = (int16_t *)&i2s_tx_buffer[AUDIO_BLOCK_SAMPLES];
		if (AudioOutputI2S::update_responsibility) AudioStream::update_all();
	} else {
		// DMA is transmitting the second half of the buffer
		// so we must fill the first half
		dest = (int16_t *)i2s_tx_buffer;
		end = (int16_t *)&i2s_tx_buffer[AUDIO_BLOCK_SAMPLES/2];
	}

	block = AudioOutputI2S::block_left_1st;
	if (block) {
		offset = AudioOutputI2S::block_left_offset;
		src = &block->data[offset];
		do {
			*dest = *src++;
			dest += 2;
		} while (dest < end);
		offset += AUDIO_BLOCK_SAMPLES/2;
		if (offset < AUDIO_BLOCK_SAMPLES) {
			AudioOutputI2S::block_left_offset = offset;
		} else {
			AudioOutputI2S::block_left_offset = 0;
			AudioStream::release(block);
			AudioOutputI2S::block_left_1st = AudioOutputI2S::block_left_2nd;
			AudioOutputI2S::block_left_2nd = NULL;
		}
	} else {
		do {
			*dest = 0;
			dest += 2;
		} while (dest < end);
	}
	dest -= AUDIO_BLOCK_SAMPLES - 1;
	block = AudioOutputI2S::block_right_1st;
	if (block) {
		offset = AudioOutputI2S::block_right_offset;
		src = &block->data[offset];
		do {
			*dest = *src++;
			dest += 2;
		} while (dest < end);
		offset += AUDIO_BLOCK_SAMPLES/2;
		if (offset < AUDIO_BLOCK_SAMPLES) {
			AudioOutputI2S::block_right_offset = offset;
		} else {
			AudioOutputI2S::block_right_offset = 0;
			AudioStream::release(block);
			AudioOutputI2S::block_right_1st = AudioOutputI2S::block_right_2nd;
			AudioOutputI2S::block_right_2nd = NULL;
		}
	} else {
		do {
			*dest = 0;
			dest += 2;
		} while (dest < end);
	}
#endif
}




void AudioOutputI2S::update(void)
{
	// null audio device: discard all incoming data
	//if (!active) return;
	//audio_block_t *block = receiveReadOnly();
	//if (block) release(block);

	audio_block_t *block;
	block = receiveReadOnly(0); // input 0 = left channel
	if (block) {
		__disable_irq();
		if (block_left_1st == NULL) {
			block_left_1st = block;
			block_left_offset = 0;
			__enable_irq();
		} else if (block_left_2nd == NULL) {
			block_left_2nd = block;
			__enable_irq();
		} else {
			audio_block_t *tmp = block_left_1st;
			block_left_1st = block_left_2nd;
			block_left_2nd = block;
			block_left_offset = 0;
			__enable_irq();
			release(tmp);
		}
	}
	block = receiveReadOnly(1); // input 1 = right channel
	if (block) {
		__disable_irq();
		if (block_right_1st == NULL) {
			block_right_1st = block;
			block_right_offset = 0;
			__enable_irq();
		} else if (block_right_2nd == NULL) {
			block_right_2nd = block;
			__enable_irq();
		} else {
			audio_block_t *tmp = block_right_1st;
			block_right_1st = block_right_2nd;
			block_right_2nd = block;
			block_right_offset = 0;
			__enable_irq();
			release(tmp);
		}
	}
}

#if defined(KINETISK) || defined(KINETISL)
// MCLK needs to be 48e6 / 1088 * 256 = 11.29411765 MHz -> 44.117647 kHz sample rate
//
#if F_CPU == 96000000 || F_CPU == 48000000 || F_CPU == 24000000
  // PLL is at 96 MHz in these modes
  #define MCLK_MULT 2
  #define MCLK_DIV  17
#elif F_CPU == 72000000
  #define MCLK_MULT 8
  #define MCLK_DIV  51
#elif F_CPU == 120000000
  #define MCLK_MULT 8
  #define MCLK_DIV  85
#elif F_CPU == 144000000
  #define MCLK_MULT 4
  #define MCLK_DIV  51
#elif F_CPU == 168000000
  #define MCLK_MULT 8
  #define MCLK_DIV  119
#elif F_CPU == 180000000
  #define MCLK_MULT 16
  #define MCLK_DIV  255
  #define MCLK_SRC  0
#elif F_CPU == 192000000
  #define MCLK_MULT 1
  #define MCLK_DIV  17
#elif F_CPU == 216000000
  #define MCLK_MULT 12
  #define MCLK_DIV  17
  #define MCLK_SRC  1
#elif F_CPU == 240000000
  #define MCLK_MULT 2
  #define MCLK_DIV  85
  #define MCLK_SRC  0
#elif F_CPU == 256000000
  #define MCLK_MULT 12
  #define MCLK_DIV  17
  #define MCLK_SRC  1
#elif F_CPU == 16000000
  #define MCLK_MULT 12
  #define MCLK_DIV  17
#else
  #error "This CPU Clock Speed is not supported by the Audio library";
#endif

#ifndef MCLK_SRC
#if F_CPU >= 20000000
  #define MCLK_SRC  3  // the PLL
#else
  #define MCLK_SRC  0  // system clock
#endif
#endif
#endif


void AudioOutputI2S::config_i2s(void)
{
#if defined(KINETISK) || defined(KINETISL)
	SIM_SCGC6 |= SIM_SCGC6_I2S;
	SIM_SCGC7 |= SIM_SCGC7_DMA;
	SIM_SCGC6 |= SIM_SCGC6_DMAMUX;

	// if either transmitter or receiver is enabled, do nothing
	if (I2S0_TCSR & I2S_TCSR_TE) return;
	if (I2S0_RCSR & I2S_RCSR_RE) return;

	// enable MCLK output
	I2S0_MCR = I2S_MCR_MICS(MCLK_SRC) | I2S_MCR_MOE;
	while (I2S0_MCR & I2S_MCR_DUF) ;
	I2S0_MDR = I2S_MDR_FRACT((MCLK_MULT-1)) | I2S_MDR_DIVIDE((MCLK_DIV-1));

	// configure transmitter
	I2S0_TMR = 0;
	I2S0_TCR1 = I2S_TCR1_TFW(1);  // watermark at half fifo size
	I2S0_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP | I2S_TCR2_MSEL(1)
		| I2S_TCR2_BCD | I2S_TCR2_DIV(1);
	I2S0_TCR3 = I2S_TCR3_TCE;
	I2S0_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(31) | I2S_TCR4_MF
		| I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_TCR4_FSD;
	I2S0_TCR5 = I2S_TCR5_WNW(31) | I2S_TCR5_W0W(31) | I2S_TCR5_FBT(31);

	// configure receiver (sync'd to transmitter clocks)
	I2S0_RMR = 0;
	I2S0_RCR1 = I2S_RCR1_RFW(1);
	I2S0_RCR2 = I2S_RCR2_SYNC(1) | I2S_TCR2_BCP | I2S_RCR2_MSEL(1)
		| I2S_RCR2_BCD | I2S_RCR2_DIV(1);
	I2S0_RCR3 = I2S_RCR3_RCE;
	I2S0_RCR4 = I2S_RCR4_FRSZ(1) | I2S_RCR4_SYWD(31) | I2S_RCR4_MF
		| I2S_RCR4_FSE | I2S_RCR4_FSP | I2S_RCR4_FSD;
	I2S0_RCR5 = I2S_RCR5_WNW(31) | I2S_RCR5_W0W(31) | I2S_RCR5_FBT(31);

	// configure pin mux for 3 clock signals
	CORE_PIN23_CONFIG = PORT_PCR_MUX(6); // pin 23, PTC2, I2S0_TX_FS (LRCLK)
	CORE_PIN9_CONFIG  = PORT_PCR_MUX(6); // pin  9, PTC3, I2S0_TX_BCLK
	CORE_PIN11_CONFIG = PORT_PCR_MUX(6); // pin 11, PTC6, I2S0_MCLK

#elif defined(__IMXRT1062__)

	CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

	// if either transmitter or receiver is enabled, do nothing
	if (I2S1_TCSR & I2S_TCSR_TE) return;
	if (I2S1_RCSR & I2S_RCSR_RE) return;
//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);

	// 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));

	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;
	//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));

#endif
}


/******************************************************************/

void AudioOutputI2Sslave::begin(void)
{

	dma.begin(true); // Allocate the DMA channel first

	block_left_1st = NULL;
	block_right_1st = NULL;

	AudioOutputI2Sslave::config_i2s();

#if defined(KINETISK)
	CORE_PIN22_CONFIG = PORT_PCR_MUX(6); // pin 22, PTC1, I2S0_TXD0
	dma.TCD->SADDR = i2s_tx_buffer;
	dma.TCD->SOFF = 2;
	dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
	dma.TCD->NBYTES_MLNO = 2;
	dma.TCD->SLAST = -sizeof(i2s_tx_buffer);
	dma.TCD->DADDR = (void *)((uint32_t)&I2S0_TDR0 + 2);
	dma.TCD->DOFF = 0;
	dma.TCD->CITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->DLASTSGA = 0;
	dma.TCD->BITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
	dma.triggerAtHardwareEvent(DMAMUX_SOURCE_I2S0_TX);
	dma.enable();

	I2S0_TCSR = I2S_TCSR_SR;
	I2S0_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;

#elif defined(__IMXRT1062__)
	CORE_PIN7_CONFIG  = 3;  //1:TX_DATA0
	dma.TCD->SADDR = i2s_tx_buffer;
	dma.TCD->SOFF = 2;
	dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
	dma.TCD->NBYTES_MLNO = 2;
	dma.TCD->SLAST = -sizeof(i2s_tx_buffer);
	dma.TCD->DOFF = 0;
	dma.TCD->CITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->DLASTSGA = 0;
	dma.TCD->BITER_ELINKNO = sizeof(i2s_tx_buffer) / 2;
	dma.TCD->DADDR = (void *)((uint32_t)&I2S1_TDR0 + 2);
	dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
	dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI1_TX);
	dma.enable();

	I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
	I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE;
#endif

	update_responsibility = update_setup();
	dma.attachInterrupt(isr);
}

void AudioOutputI2Sslave::config_i2s(void)
{
#if defined(KINETISK)
	SIM_SCGC6 |= SIM_SCGC6_I2S;
	SIM_SCGC7 |= SIM_SCGC7_DMA;
	SIM_SCGC6 |= SIM_SCGC6_DMAMUX;

	// if either transmitter or receiver is enabled, do nothing
	if (I2S0_TCSR & I2S_TCSR_TE) return;
	if (I2S0_RCSR & I2S_RCSR_RE) return;

	// Select input clock 0
	// Configure to input the bit-clock from pin, bypasses the MCLK divider
	I2S0_MCR = I2S_MCR_MICS(0);
	I2S0_MDR = 0;

	// configure transmitter
	I2S0_TMR = 0;
	I2S0_TCR1 = I2S_TCR1_TFW(1);  // watermark at half fifo size
	I2S0_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP;

	I2S0_TCR3 = I2S_TCR3_TCE;
	I2S0_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(31) | I2S_TCR4_MF
		| I2S_TCR4_FSE | I2S_TCR4_FSP;

	I2S0_TCR5 = I2S_TCR5_WNW(31) | I2S_TCR5_W0W(31) | I2S_TCR5_FBT(31);

	// configure receiver (sync'd to transmitter clocks)
	I2S0_RMR = 0;
	I2S0_RCR1 = I2S_RCR1_RFW(1);
	I2S0_RCR2 = I2S_RCR2_SYNC(1) | I2S_TCR2_BCP;

	I2S0_RCR3 = I2S_RCR3_RCE;
	I2S0_RCR4 = I2S_RCR4_FRSZ(1) | I2S_RCR4_SYWD(31) | I2S_RCR4_MF
		| I2S_RCR4_FSE | I2S_RCR4_FSP | I2S_RCR4_FSD;

	I2S0_RCR5 = I2S_RCR5_WNW(31) | I2S_RCR5_W0W(31) | I2S_RCR5_FBT(31);

	// configure pin mux for 3 clock signals
	CORE_PIN23_CONFIG = PORT_PCR_MUX(6); // pin 23, PTC2, I2S0_TX_FS (LRCLK)
	CORE_PIN9_CONFIG  = PORT_PCR_MUX(6); // pin  9, PTC3, I2S0_TX_BCLK
	CORE_PIN11_CONFIG = PORT_PCR_MUX(6); // pin 11, PTC6, I2S0_MCLK

#elif defined(__IMXRT1062__)

	CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);

	// if either transmitter or receiver is enabled, do nothing
	if (I2S1_TCSR & I2S_TCSR_TE) return;
	if (I2S1_RCSR & I2S_RCSR_RE) return;

	// not using MCLK in slave mode - hope that's ok?
	//CORE_PIN23_CONFIG = 3;  // AD_B1_09  ALT3=SAI1_MCLK
	CORE_PIN21_CONFIG = 3;  // AD_B1_11  ALT3=SAI1_RX_BCLK
	CORE_PIN20_CONFIG = 3;  // AD_B1_10  ALT3=SAI1_RX_SYNC
	IOMUXC_SAI1_RX_BCLK_SELECT_INPUT = 1; // 1=GPIO_AD_B1_11_ALT3, page 868
	IOMUXC_SAI1_RX_SYNC_SELECT_INPUT = 1; // 1=GPIO_AD_B1_10_ALT3, page 872

	// configure transmitter
	I2S1_TMR = 0;
	I2S1_TCR1 = I2S_TCR1_RFW(1);  // watermark at half fifo size
	I2S1_TCR2 = I2S_TCR2_SYNC(1) | I2S_TCR2_BCP;
	I2S1_TCR3 = I2S_TCR3_TCE;
	I2S1_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(31) | I2S_TCR4_MF
		| I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_RCR4_FSD;
	I2S1_TCR5 = I2S_TCR5_WNW(31) | I2S_TCR5_W0W(31) | I2S_TCR5_FBT(31);

	// configure receiver
	I2S1_RMR = 0;
	I2S1_RCR1 = I2S_RCR1_RFW(1);
	I2S1_RCR2 = I2S_RCR2_SYNC(0) | I2S_TCR2_BCP;
	I2S1_RCR3 = I2S_RCR3_RCE;
	I2S1_RCR4 = I2S_RCR4_FRSZ(1) | I2S_RCR4_SYWD(31) | I2S_RCR4_MF
		| I2S_RCR4_FSE | I2S_RCR4_FSP;
	I2S1_RCR5 = I2S_RCR5_WNW(31) | I2S_RCR5_W0W(31) | I2S_RCR5_FBT(31);

#endif
}

Revised output_i2s.cpp.
 
I have it working, with 2 DMA channels, 2IRQ, and without the double-buffering.
There were some details that were not that easy to find.
- For example, the I2S has no FRDE bit.
- I had to use 16 Bit DMA transfers (I'll look if I can change that. But it is not that important.)

I'll clean up my sourcecode now, and try to optimize some things before I do a Github pullrequest for Paul.
 
Thanks Frank.
When I copy the new files and compile the sketch on top of this thread, I see this on the scope [measured at the output connector of the PT8211]:

Left channel
Left.png

Right channel
Right.png

I will have a look at your files now.

Paul
 
That solved it!

SDS00010.png

And still some memory left:
Code:
Sketch uses 18284 bytes (28%) of program storage space. Maximum is 63488 bytes.
Global variables use 5296 bytes (64%) of dynamic memory, leaving 2896 bytes for local variables. Maximum is 8192 bytes.

Thanks,
Paul
 
Back
Top