SPDIF Input and Output

Status
Not open for further replies.

gfvalvo

Well-known member
Hi all. A while back I was working on an audio-reactive LED project. Because of the physical location, the most convenient way to bring the audio to the Teensy was via an optical SPDIF connection. I started working with the TI DIR9001 mostly because it’s available on a preassembled eval board containing all required components for Optical SPDIF --> I2S conversion. You just connect power, the optical input, and the I2S signals. I posted the resulting code in this forum

The main drawbacks of the DIR9001 are that it’s unidirectional only (it can’t do I2S --> SPDIF) and the eval board is ridiculously expensive.

Then I ran across the WM8804 from Cirrus Logic. This device solves both of the above problems as it is bidirectional and there is a very reasonably-priced board available.

So, I created several new classes based on the Quad I2S classes in the Audio Library. The major changes are accommodating a BCLK frequency of 64 * Fs and support for Teensy to operate in I2S Slave mode. The library code and two examples are attached. The connections supporting the examples are:

I2S Connections.jpg

As when using dual Audio Adaptor Boards, you need to disconnect Teensy Pin 15 from Audio Adaptor Pin 15. Otherwise, the bypass cap on the later kills the high frequency IS0_TXD1 signal. I cut the pin, you can probably also just remove the cap.

When a SPDIF input is present, the WM8804 must act as I2S master to synch up MCLK, BCLK, and LRCLK with this input. In this case, the I2S interfaces in the Teensy are set for Slave mode and all Audio object timing is frequency synchronous with the WM8804. The 'Quad_I2S_SlaveExample' example blends audio from 2 independent sources -- SPDIF / WM8804 and the SGTL5000's ADC on the Audio Adaptor. It then outputs the resulting signal to 3 destinations -- back to the WM8804 board for I2S --> SPDIF Out conversion, to the DAC on the SGTL5000, and to Teensy's onboard DAC. The pot sets the blending level between the two sources. The connections from the Audio Design tool look like this:

Audio Tool - Teensy Slave.jpg

When there is no SPDIF input but SPDIF output is desired, then the WM8804 is set for Slave mode and Teensy becomes the I2S master. All Audio object timing is now derived from Teensy's clock. The 'Quad_I2S_MasterExample' accepts I2S audio input from the SGTL5000 and outputs it to the same 3 destinations as above. The connections from the Audio Design tool look like this:

Audio Tool - Teensy Master.jpg

The only real drawback of this WM8804 board is the documentation -- there isn't any. Well, there is a short User Manual, but it's written in Chinglish and is essentially useless. The company won't release the schematic either which would be helpful in figuring out the switch settings. But after a little head scratching I worked it out. The WM8804 datasheet is pretty good.

Anyway, I hope someone finds this useful.



PS -- There is a short description of each new class in the respective .h files -- just under the PJRC copyright notice.
 

Attachments

  • WM8804.zip
    91.4 KB · Views: 305
The major changes are accommodating a BCLK frequency of 64 * Fs and support for Teensy to operate in I2S Slave mode.

The I2S slave mode object was recently improved and changed to 64 bits per frame. It's available in 1.38-beta2.

Any chance you could give the latest beta a try? The only reason I2S slave mode was broken for so long (before 1.38-beta) was lack of usage and feedback....
 
I can give it a look when I get a chance. What’s the best way to try a Beta version without upsetting my “production” installation? I’m currently at Arduino 1.8.2, Teensyduino 1.36 (I know I should go to Teensyduino 1.37, just haven’t gotten around to it).
 
Just extract the Arduino ZIP file somewhere else on your computer.

If using Windows, you need to download the ZIP file. Don't use the Arduino windows installer.

When you run the Teensyduino installer, of course you'll need to know where the other copy is. Pay attention to the folder location, since the Teensyduino installer may find and auto-select you first copy, especially if it's in the default location.
 
OK, will give it a shot.

I just had a quick look at the source code on the github. You used a cleverer technique for 64 bits than I did. I used brute force and just set the DMA for 32 bit transfers each of Left and Right channels then threw away the unused 16 bits in the ISR.

Anyway, looking at control_sgtl5000.cpp, I’m wondering if you’re going to need to update the AudioControlSGTL5000::enable() to set the device for 64 * Fs also?
Code:
write(CHIP_I2S_CTRL, 0x0030);
Instead of
Code:
write(CHIP_I2S_CTRL, 0x0130);
 
OK Paul, by removing the Audio Adaptor and connecting the WM8804 EVB to TX_BCLK, TX_FS, I2S0_RXD0, and I2S0_TXD0 the new 64-bit I2S Slave classes worked as expected. The WM8804 was I2S bus master with timing based on the supplied Optical SPDIF signal. The T3.2 received the audio and sent it back out to the WM8804 (for I2S --> Optical SPDIF conversion) and to the Teensy's onboard DAC.

Here's the Design Tool setup:

Design Tool.jpg

Here's the sketch:
Code:
#include <Audio.h>
#include "output_DacSyncLRCLK.h"

AudioInputI2Sslave           i2sslave1;      //xy=623,453
AudioMixer4                  mixer1;         //xy=826,465
AudioOutputI2Sslave          i2sslave2;      //xy=838,349
AudioOutputAnalogSyncLRCLK   dac1;           //xy=997,468
AudioAnalyzeFFT1024          fft1024_1;      //xy=1004,547
AudioConnection              patchCord1(i2sslave1, 0, i2sslave2, 0);
AudioConnection              patchCord2(i2sslave1, 0, mixer1, 0);
AudioConnection              patchCord3(i2sslave1, 1, i2sslave2, 1);
AudioConnection              patchCord4(i2sslave1, 1, mixer1, 1);
AudioConnection              patchCord5(mixer1, dac1);
AudioConnection              patchCord6(mixer1, fft1024_1);

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting");
  AudioMemory(15);
  AudioOutputAnalogSyncLRCLK::enableDmaRequest();
  mixer1.gain(0, 0.5);
  mixer1.gain(1, 0.5);
  mixer1.gain(2, 0.0);
  mixer1.gain(3, 0.0);
}

void loop() {
#if 1
  // print Fourier Transform data to the Arduino Serial Monitor
  if (fft1024_1.available()) {

    Serial.print("FFT: ");
    for (int i = 0; i < 30; i++) {
      float n = fft1024_1.read(i);
      printNumber(n);
    }
    Serial.println();
  }
#endif
}

void printNumber(float n) {
  if (n >= 0.004) {
    Serial.print(n, 3);
    Serial.print(" ");
  } else {
    Serial.print("   -  "); // don't print "0.00"
  }
}

Note that I used a modified version of the AudioOutputAnalog class to run the onboard DAC. It triggers the DAC's DMA operation from LRCLK (aka FS) rather than the Teensy's clock. That way it stays synchronized with the audio from the WM8804. Here are the .h and .cpp files:
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.
*/

// Modification of standard AudioOutputAnalog class to trigger DMA operations
// from I2S LRCLK rather than from the programmable delay block.
// To enable, must call enableDmaRequest after I2S constructors run.
// Good place to do this is setup().


#ifndef output_dac_synch_LRCLK_h_
#define output_dac_synch_LRCLK_h_

#include "Arduino.h"
#include "AudioStream.h"
#include "DMAChannel.h"


class AudioOutputAnalogSyncLRCLK : public AudioStream
{
  public:
    AudioOutputAnalogSyncLRCLK(void) : AudioStream(1, inputQueueArray) {
      begin();
    }
    virtual void update(void);
    void begin(void);
    void analogReference(int ref);
    static void enableDmaRequest(void);

  private:
    static audio_block_t *block_left_1st;
    static audio_block_t *block_left_2nd;
    static bool update_responsibility;
    audio_block_t *inputQueueArray[1];
#if defined(KINETISK)
    static DMAChannel dma;
    static void isr(void);
#elif defined(KINETISL)
    static DMAChannel dma1;
    static DMAChannel dma2;
    static void isr1(void);
    static void isr2(void);
#endif
};
#endif

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 "output_DacSyncLRCLK.h"

#if !defined(KINETISK)
#error Unsupported Processor
#endif

DMAMEM static uint16_t dac_buffer[AUDIO_BLOCK_SAMPLES*2];
audio_block_t * AudioOutputAnalogSyncLRCLK::block_left_1st = NULL;
audio_block_t * AudioOutputAnalogSyncLRCLK::block_left_2nd = NULL;
bool AudioOutputAnalogSyncLRCLK::update_responsibility = false;
DMAChannel AudioOutputAnalogSyncLRCLK::dma(false);

void AudioOutputAnalogSyncLRCLK::begin(void)
{
	dma.begin(true); // Allocate the DMA channel first
	SIM_SCGC2 |= SIM_SCGC2_DAC0;
	DAC0_C0 = DAC_C0_DACEN;                   // 1.2V VDDA is DACREF_2
	// slowly ramp up to DC voltage, approx 1/4 second
	for (int16_t i=0; i<2048; i+=8) {
		*(int16_t *)&(DAC0_DAT0L) = i;
		delay(1);
	}

  // Configure to trigger DMA using LRCLK on Pin 29
  SIM_SCGC5 |= SIM_SCGC5_PORTC;
  SIM_SCGC6 |= SIM_SCGC6_DMAMUX;
  SIM_SCGC7 |= SIM_SCGC7_DMA;
 
	dma.TCD->SADDR = dac_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(dac_buffer);
	dma.TCD->DADDR = &DAC0_DAT0L;
	dma.TCD->DOFF = 0;
	dma.TCD->CITER_ELINKNO = sizeof(dac_buffer) / 2;
	dma.TCD->DLASTSGA = 0;
	dma.TCD->BITER_ELINKNO = sizeof(dac_buffer) / 2;
	dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
  dma.triggerAtHardwareEvent(DMAMUX_SOURCE_PORTC);
	update_responsibility = update_setup();
	dma.enable();
	dma.attachInterrupt(isr);
}

void AudioOutputAnalogSyncLRCLK::analogReference(int ref)
{
	// TODO: this should ramp gradually to the new DC level
	if (ref == INTERNAL) {
		DAC0_C0 &= ~DAC_C0_DACRFS; // 1.2V
	} else {
		DAC0_C0 |= DAC_C0_DACRFS;  // 3.3V
	}
}


void AudioOutputAnalogSyncLRCLK::update(void)
{
	audio_block_t *block;
	block = receiveReadOnly(0); // input 0
	if (block) {
		__disable_irq();
		if (block_left_1st == NULL) {
			block_left_1st = block;
			__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;
			__enable_irq();
			release(tmp);
		}
	}
}

void AudioOutputAnalogSyncLRCLK::isr(void)
{
	const int16_t *src, *end;
	int16_t *dest;
	audio_block_t *block;
	uint32_t saddr;

	saddr = (uint32_t)(dma.TCD->SADDR);
	dma.clearInterrupt();
	if (saddr < (uint32_t)dac_buffer + sizeof(dac_buffer) / 2) {
		// DMA is transmitting the first half of the buffer
		// so we must fill the second half
		dest = (int16_t *)&dac_buffer[AUDIO_BLOCK_SAMPLES];
		end = (int16_t *)&dac_buffer[AUDIO_BLOCK_SAMPLES*2];
	} else {
		// DMA is transmitting the second half of the buffer
		// so we must fill the first half
		dest = (int16_t *)dac_buffer;
		end = (int16_t *)&dac_buffer[AUDIO_BLOCK_SAMPLES];
	}
	block = AudioOutputAnalogSyncLRCLK::block_left_1st;
	if (block) {
		src = block->data;
		do {
			// TODO: this should probably dither
			*dest++ = ((*src++) + 32767) >> 4;
		} while (dest < end);
		AudioStream::release(block);
		AudioOutputAnalogSyncLRCLK::block_left_1st = AudioOutputAnalogSyncLRCLK::block_left_2nd;
		AudioOutputAnalogSyncLRCLK::block_left_2nd = NULL;
	} else {
		do {
			*dest++ = 2047;
		} while (dest < end);
	}
	if (AudioOutputAnalogSyncLRCLK::update_responsibility) AudioStream::update_all();
}

void AudioOutputAnalogSyncLRCLK::enableDmaRequest() {
  // Enable PIN29 to trigger DMA
  // Call this during setup() after we're sure AudioInputWM8804Slave constructor has run
  CORE_PIN23_CONFIG |= PORT_PCR_IRQC(2); // pin 29, PTC10, I2S0_RX_FS
}

Finally, although the new classes worked, they didn't provide all the functionality of the example I provided. I wanted to take input from both the WM8804 and the SGTL5000 and then turn it around in the Teensy and send it back to the same devices. For that I needed the 64-bit Quad I2S Slave classes that I attached in my original post. I also attached a 64-bit Quad I2S Master class. This was Output only as you can't get input from the WM8804 unless it is the I2S Master.
 
Status
Not open for further replies.
Back
Top