Realtime S/PDIF decoding on Teensy 4.0

Status
Not open for further replies.

rkmaddox

Member
Hello,

I work in a neuroscience lab studying the auditory system, and we use TTL trigger pulses to sync our audio stimuli with the EEG system. The way I do this is through simultaneous analog and S/PDIF audio from the same soundcard, with analog going to headphones and S/PDIF int values being split by a custom box into separate TTL bit lines. My current device requires a custom PCB to be made and populated, but I'm hoping the Teensy 4.0's S/PDIF input could replace it.

Basically, every time a new S/PDIF sample arrives, the device would write its bits (or a subset of them) out to the GPIO. It would need to do this in real time, but I am hopeful it could work since that is all the system needs to do. It would need to work with 44.1 or 48 kHz and 16 or 24 bit audio.

Is this possible? Is there any example code that reads the latest sample, perhaps triggered by an interrupt? Or perhaps the S/PDIF samples go to a register I can poll? I have read up on the audio library, but I don't believe there is a way to use that in real time since it's designed to do more than just pass samples through.

Any guidance appreciated!
 
It needs to do the operation on every incoming sample (so 44.1k or 48k times per second). So no frames like in normal applications. Latency, as long as it's consistent (jitter < .05 ms), is not a concern, since it can be measured and compensated.
 
Thanks. From reading it seems like it should be possible. I've never programmed at this level before, but I'll have a go at it. Time to read up on interrupts.
 
That's the easy part. attachInterrupt()
Then, you can take a look at the audio-library SPDIF input and look at the code. Remove all the DMA stuff, and it is almost done..
 
This is probably a dumb question--but how do I access the specific interrupts and registers that are named in the manual? Since I'm sure this has been described a million times a link is fine.

From the manual it looks as if the relevant interrupt is called "QChannelRxFull," but that doesn't show up anywhere in the code, nor does the name of the register "QChannelReceive" or maybe "UChannelReceive."

I'll keep digging. Thanks for the help so far.
 
Ahhh those names aren't exactly how things are accessed in the code. For example the SPDIFRxLeft Register is read from SPDIF_SRL.
 
So I have my teensy and have made some progress in understanding the registers. However, when I set any of them I get upload errors. The code (which at this point just sends a byte out to a shift register through SPI to control some LEDs) works fine if all the lines that set SPDIF_SCR are commented out. But if any of them are uncommented then I get upload errors. Specifically, it uploads one time but the LEDs no longer blink. And any attempts to upload again require closing teensyduino and re-plugging the USB cable with the reset button held down. Is there something wrong I'm doing?

Code below (in its working state with S/PDIF register lines commented out):
Code:
#include <SPI.h>

#if defined(__IMXRT1062__)
extern "C" uint32_t set_arm_clock(uint32_t frequency);
#endif

#define SPDIF_PIN_MODE 3

// Set some pin names
#define REG_CLEAR_PIN 8
#define SHIFT_ENABLE_PIN 9
#define LATCH_PIN 10
#define SPI_MOSI_PIN 11
#define SPI_CLK_PIN 13
#define LED_POW_PIN 16
#define LED_SPDIF_PIN 17

void setup() {
  //set_arm_clock(400000000); // underclock Teensy to 400 MHz to save a bit of power
  
  // Set up S/PDIF ========
  // SPDIF_SCR = 0x0000400; // reset SPDIF configuration register with Tx normal (default)
  //SPDIF_SCR = 0x0; // reset SPDIF configuration register with Tx send digital zeros
  //SPDIF_SCR |= SPDIF_SCR_RXAUTOSYNC; // turn on s/pdif Rx autosync
  //SPDIF_SCR &= ~SPDIF_SCR_RXFIFO_OFF_ON; // want it set to 0: Rx FIFO is on
  
  CORE_PIN15_CONFIG = SPDIF_PIN_MODE; // set pin 15 to s/pdif input
  // RxFIFOFull_Sel This one has four options. Want it set to 00 - 1 sample in FIFO
  
  
  // Set up SPI shift registers ========
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(SHIFT_ENABLE_PIN, OUTPUT);
  pinMode(REG_CLEAR_PIN, OUTPUT);
  pinMode(LED_POW_PIN, OUTPUT);
  pinMode(LED_SPDIF_PIN, OUTPUT);

  digitalWrite(SHIFT_ENABLE_PIN, LOW); // enable low
  digitalWrite(REG_CLEAR_PIN, LOW); // clear low
  digitalWrite(REG_CLEAR_PIN, HIGH); // clear low

  digitalWrite(LED_POW_PIN, HIGH);
  SPI.begin();
}

byte shift_val = 0;
void loop() {
  // Send the bit values out to the shift registers using SPI
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
  digitalWriteFast(LATCH_PIN, LOW);
  SPI.transfer(shift_val++);
  digitalWriteFast(LATCH_PIN, HIGH); 
  SPI.endTransaction();
  if(shift_val & 128) {
    digitalWriteFast(LED_POW_PIN, HIGH);
  } else {
    digitalWriteFast(LED_POW_PIN, LOW);
  }
   
  delay(50);
}
 
You need to turn on the clock to the SPDIF peripheral before you attempt to use it.

Here's your program with the clock config copied from the Audio library. It starts up with those 4 lines uncommented.

Code:
#include <SPI.h>

#if defined(__IMXRT1062__)
extern "C" uint32_t set_arm_clock(uint32_t frequency);
#endif

#define SPDIF_PIN_MODE 3

// Set some pin names
#define REG_CLEAR_PIN 8
#define SHIFT_ENABLE_PIN 9
#define LATCH_PIN 10
#define SPI_MOSI_PIN 11
#define SPI_CLK_PIN 13
#define LED_POW_PIN 16
#define LED_SPDIF_PIN 17

void setup() {
  //set_arm_clock(400000000); // underclock Teensy to 400 MHz to save a bit of power

  int n1 = 7; //0: divide by 1 (do not use with high input frequencies), 1:/2, 2: /3, 7:/8
  int n2 = 0; //0: divide by 1, 7: divide by 8

  CCM_CCGR5 &= ~CCM_CCGR5_SPDIF(CCM_CCGR_ON); //Clock gate off

  CCM_CDCDR = (CCM_CDCDR & ~(CCM_CDCDR_SPDIF0_CLK_SEL_MASK | CCM_CDCDR_SPDIF0_CLK_PRED_MASK | CCM_CDCDR_SPDIF0_CLK_PODF_MASK))
              | CCM_CDCDR_SPDIF0_CLK_SEL(0) // 0 PLL4, 1 PLL3 PFD2, 2 PLL5, 3 pll3_sw_clk
              | CCM_CDCDR_SPDIF0_CLK_PRED(n1)
              | CCM_CDCDR_SPDIF0_CLK_PODF(n2);

  CCM_CCGR5 |= CCM_CCGR5_SPDIF(CCM_CCGR_ON); //Clock gate on


  // Set up S/PDIF ========
  SPDIF_SCR = 0x0000400; // reset SPDIF configuration register with Tx normal (default)
  SPDIF_SCR = 0x0; // reset SPDIF configuration register with Tx send digital zeros
  SPDIF_SCR |= SPDIF_SCR_RXAUTOSYNC; // turn on s/pdif Rx autosync
  SPDIF_SCR &= ~SPDIF_SCR_RXFIFO_OFF_ON; // want it set to 0: Rx FIFO is on

  CORE_PIN15_CONFIG = SPDIF_PIN_MODE; // set pin 15 to s/pdif input
  // RxFIFOFull_Sel This one has four options. Want it set to 00 - 1 sample in FIFO


  // Set up SPI shift registers ========
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(SHIFT_ENABLE_PIN, OUTPUT);
  pinMode(REG_CLEAR_PIN, OUTPUT);
  pinMode(LED_POW_PIN, OUTPUT);
  pinMode(LED_SPDIF_PIN, OUTPUT);

  digitalWrite(SHIFT_ENABLE_PIN, LOW); // enable low
  digitalWrite(REG_CLEAR_PIN, LOW); // clear low
  digitalWrite(REG_CLEAR_PIN, HIGH); // clear low

  digitalWrite(LED_POW_PIN, HIGH);
  SPI.begin();
  while (!Serial && millis() < 4000) ; // wait (up to 4 seconds) for Arduino Serial Monitor
  Serial.println("started up");
}

byte shift_val = 0;
void loop() {
  // Send the bit values out to the shift registers using SPI
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
  digitalWriteFast(LATCH_PIN, LOW);
  SPI.transfer(shift_val++);
  digitalWriteFast(LATCH_PIN, HIGH);
  SPI.endTransaction();
  if (shift_val & 128) {
    digitalWriteFast(LED_POW_PIN, HIGH);
  } else {
    digitalWriteFast(LED_POW_PIN, LOW);
  }

  delay(50);
}

Maybe look at the Audio library code. There's probably more good examples in there to save you a lot of time.
 
That works again. The audio code has been very helpful. I missed this because it was in the output code and my application only uses s/pdif input. I should have looked there, too, though.

Thanks for the help!
 
I believe I am very close to having this working. The last issue seems to be that my interrupt service routine is not getting called. When I take the code from the interrupt and put it into the loop, it operates as hoped, though that seems like a dodgy way to do it. I've googled extensively and not found the solution, so I apologize if it's something obvious I'm not understanding. Here is the code:

Code:
#include <SPI.h>

#if defined(__IMXRT1062__)
extern "C" uint32_t set_arm_clock(uint32_t frequency);
#endif

#define SPDIF_PIN_MODE 3

// Set some pin names
#define REG_CLEAR_PIN 8
#define SHIFT_ENABLE_PIN 9
#define LATCH_PIN 10
#define SPI_MOSI_PIN 11
#define SPI_CLK_PIN 13
#define LED_POW_PIN 16
#define LED_SPDIF_PIN 17

volatile byte fifo_left[3];
volatile byte fifo_right[3];
void fifo_isr() {
  fifo_left[0] = SPDIF_SRL >> 0;
  fifo_left[1] = SPDIF_SRL >> 8;
  fifo_left[2] = SPDIF_SRL >> 16;
  fifo_right[0] = SPDIF_SRR >> 0;
  fifo_right[1] = SPDIF_SRR >> 8;
  fifo_right[2] = SPDIF_SRR >> 16;
  
  SPI.beginTransaction(SPISettings(100000000, MSBFIRST, SPI_MODE0));
  digitalWriteFast(LATCH_PIN, LOW);
  
  SPI.transfer(fifo_right[2]);
  SPI.transfer(fifo_right[1]);
  SPI.transfer(fifo_right[0]);
  SPI.transfer(fifo_left[2]);
  SPI.transfer(fifo_left[1]);
  SPI.transfer(fifo_left[0]);
  
  digitalWriteFast(LATCH_PIN, HIGH); 
  SPI.endTransaction();

  digitalWriteFast(LED_POW_PIN, LOW); // to test if the ISR is getting called
};

void setup() {
  set_arm_clock(500000000); // underclock Teensy to 400 MHz to save a bit of power

  // Initialize the S/PDIF clock ======== This might need to be modified to take the clock from S/PDIF in
  int n1 = 7; //0: divide by 1 (do not use with high input frequencies), 1:/2, 2: /3, 7:/8
  int n2 = 0; //0: divide by 1, 7: divide by 8

  CCM_CCGR5 &= ~CCM_CCGR5_SPDIF(CCM_CCGR_ON); //Clock gate off
  CCM_CDCDR = (CCM_CDCDR & ~(CCM_CDCDR_SPDIF0_CLK_SEL_MASK | CCM_CDCDR_SPDIF0_CLK_PRED_MASK | CCM_CDCDR_SPDIF0_CLK_PODF_MASK))
              | CCM_CDCDR_SPDIF0_CLK_SEL(0) // 0 PLL4, 1 PLL3 PFD2, 2 PLL5, 3 pll3_sw_clk
              | CCM_CDCDR_SPDIF0_CLK_PRED(n1)
              | CCM_CDCDR_SPDIF0_CLK_PODF(n2);
  CCM_CCGR5 |= CCM_CCGR5_SPDIF(CCM_CCGR_ON); //Clock gate on
  
  // Set up S/PDIF registers ========
  SPDIF_SRPC = SPDIF_SRPC_CLKSRC_SEL(1); //if (DPLL Locked) SPDIF_RxClk else tx_clk (SPDIF0_CLK_ROOT)
  // SPDIF_SCR = 0x0000400; // reset SPDIF configuration register with Tx normal (default)
  SPDIF_SCR = 0x0; // reset SPDIF configuration register with Tx send digital zeros
  SPDIF_SCR |= SPDIF_SCR_RXAUTOSYNC; // turn on s/pdif Rx autosync
  SPDIF_SCR &= ~SPDIF_SCR_RXFIFO_OFF_ON; // want it set to 0: Rx FIFO is on
  
  CORE_PIN15_CONFIG = SPDIF_PIN_MODE; // set pin 15 to s/pdif input
  IOMUXC_SPDIF_IN_SELECT_INPUT = 0; // GPIO_AD_B1_03_ALT3 - taken from input_spdif
  // RxFIFOFull_Sel This one has four options. Want it set to 00 - 1 sample in FIFO, the default
  // use SPDIF_SCR_RXFIFOFULL_SEL(0);

  // Enable S/PDIF FIFO full interrupt (bit 0)
  //SPDIF Rx FIFO full, can't be cleared with reg. IntClear. To clear it, read from Rx FIFO.
  SPDIF_SIE = 1;
  attachInterrupt(IRQ_SPDIF, fifo_isr, HIGH);
  
  // Set up SPI shift registers ========
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(SHIFT_ENABLE_PIN, OUTPUT);
  pinMode(REG_CLEAR_PIN, OUTPUT);
  pinMode(LED_POW_PIN, OUTPUT);
  pinMode(LED_SPDIF_PIN, OUTPUT);

  digitalWrite(SHIFT_ENABLE_PIN, LOW); // enable low
  digitalWrite(REG_CLEAR_PIN, LOW); // clear low
  digitalWrite(REG_CLEAR_PIN, HIGH); // clear low

  digitalWrite(LED_POW_PIN, HIGH);
  SPI.begin();
  SPI.usingInterrupt(IRQ_SPDIF);
}

void loop() {
  digitalWriteFast(LED_SPDIF_PIN, (SPDIF_SRPC & SPDIF_SRPC_LOCK) == SPDIF_SRPC_LOCK);
}
 
Status
Not open for further replies.
Back
Top