Introducing the T-DSP TAC5212 Pro Audio Module – Help Needed Getting Clock Sync on Teensy 4.1

JayShoe

Well-known member
Hi everyone,

TL;DR: Built a modular Teensy-based audio dev board using the TAC5212 codec. Getting sound, but clocks aren't locking. Looking for help debugging TDM slave clock setup.

I'm excited to share the first prototype of my open modular digital audio platform: T-DSP. The project is aimed at musicians, tinkerers, and audio professionals who want powerful, hackable audio tools. The T-DSP TAC5212 Pro Audio Module will be available commercially and is not released as open hardware. Reference hardware designs, such as the backplanes, are released under Creative Commons BY-NC-SA 4.0. Software will be open-source and released under the MIT License. The project will be available as both DIY modules and ready-to-use gear through t-dsp.com (coming soon!).

This post is specifically about the T-DSP TAC5212 Pro Audio Module, which is at the heart of my modular design strategy. The module can be used by itself or in multiples, and it can be daisy-chained through a custom backplane or 20-pin IDE cable. This modular strategy makes designing audio backplanes easy—enabling all sorts of opportunities for custom audio systems.

The first backplane, which I’m calling my "desktop soundcard" backplane, features a Teensy 4.1, ESP32 DevKitC, and a T-DSP TAC5212 Pro Audio Module, LEDs, a rotary encoder, a headphone output, a headphone input, onboard microphones, and other features I’ll talk more about later.

For now, I’m reaching out for help getting the clocks to sync.

🎧 What Works:​

I’m able to get audio out of the headphone jack using a Teensy 4.1 and a minimal sketch (shared below). That gives me confidence that the hardware and signal path are mostly correct.

⚠️ What’s Not Working:​

The sound has some crackling, and the codec clock doesn't seem to be locking properly. I’m still tweaking the BCLK, MCLK, and frame sync settings and would greatly appreciate help verifying the correct clock configuration for the TAC5212 when it's set up as a TDM slave.

I’ve read through the TAC5212 datasheet, but the clocking diagrams are a bit difficult to decipher at first glance, and I’m not sure how they compare to how the Teensy generally handles things. I don’t know if I’m missing something obvious in my Teensy setup or if the codec needs a different initialization sequence, clock polarity, RX Offset, or some other configuration.

📸 Photo of the setup:​


T-DSP-TAC5212_Pro_Audio_Module_Desktop_Soundcard_Prototype_3.jpg


💾 Code​

Here’s the minimal Teensy code I’m using right now. This setup allows two sine waves to play through the headphones, but the output is sloppy and crackly. Also, I wind up getting Clock Status (0x13): 0x0 | Clocks not locked errors. I’d love feedback if you notice anything off or missing.

C++:
#include <Arduino.h>
#include <Wire.h>
#include <Audio.h>

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

#define TAC5212_ADDR 0x50

AudioSynthWaveformSine sine1;
AudioSynthWaveformSine sine2;
AudioOutputTDM tdmOut;
AudioInputUSB            usb1;           //xy=109.33333969116211,45.000003814697266




AudioConnection          patchCord1(sine1, 0, tdmOut, 0);
//AudioConnection          patchCord2(sine1, 0, tdmOut, 1);
AudioConnection          patchCord3(sine2, 0, tdmOut, 2);
//AudioConnection          patchCord4(sine1, 0, tdmOut, 3);
// AudioConnection          patchCord5(sine2, 0, tdmOut, 4);
//AudioConnection          patchCord6(sine1, 0, tdmOut, 5);
// AudioConnection          patchCord7(sine2, 0, tdmOut, 6);
//AudioConnection          patchCord8(sine1, 0, tdmOut, 7);
// AudioConnection          patchCord9(sine2, 0, tdmOut, 8);

//AudioConnection patchCord2(sine2, 0, tdmOut, 0);
AudioControlSGTL5000 dummyControl;

void writeReg(uint8_t reg, uint8_t val) {
  Wire.beginTransmission(TAC5212_ADDR);
  Wire.write(reg);
  Wire.write(val);
  Wire.endTransmission();
}

uint8_t readReg(uint8_t reg) {
  Wire.beginTransmission(TAC5212_ADDR);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(TAC5212_ADDR, 1);
  return Wire.available() ? Wire.read() : 0xFF;
}

void dumpRegisters(uint8_t count = 64) {
  Serial.println("🧾 Dumping first codec registers:");
  for (uint8_t r = 0; r < count; r++) {
    uint8_t val = readReg(r);
    Serial.print("0x"); if (r < 0x10) Serial.print("0");
    Serial.print(r, HEX);
    Serial.print(": 0x"); if (val < 0x10) Serial.print("0");
    Serial.println(val, HEX);
    delay(5);
  }
}

void setupCodec(bool usePLL, uint8_t pasi_cfg0) {
  writeReg(0x01, 0x01); delay(100); // Reset
  writeReg(0x02, 0x09); delay(100); // Wakeup

  if (usePLL) {
    Serial.println("🔧 Enabling PLL Mode");
    writeReg(0x10, 0x01); // PLL Enable
    writeReg(0x11, 0x30); // PLL Config
    delay(50);
  } else {
    Serial.println("🎧 Using External Clock Mode");
  }

  //writeReg(0x19, 0x10);     // ASI_CFG1: TDM Mode
  // writeReg(0x1A, 0x00); // PASI_CFG0: frame size = 16 slots
  writeReg(0x19, 0x13); // 0x10 | 0x03: TDM mode + 32-bit words
  writeReg(0x26, 0x00); // Set PASI_RX_OFFSET to 1

  //writeReg(0x1A, pasi_cfg0);// PASI_CFG0
  //writeReg(0x26, 0x00);     // Slot 0 


  // DAC Time Slot Assignments
writeReg(0x41, 0);  // DAC L1 -> Slot 0
writeReg(0x42, 0);  // DAC R1 -> Slot 1
writeReg(0x43, 0);  // DAC L2 -> Slot 0
writeReg(0x44, 0);  // DAC R2 -> Slot 1

// writeReg(0x12, 0x01);     // Accept external clock

  // Unmute and set gain
  // writeReg(0x40, 128);     // Unmute (if supported)
  writeReg(0x67, 128);     // DAC L1 Vol
  writeReg(0x69, 128);     // DAC R1 Vol
  writeReg(0x6E, 128);     // DAC L2 Vol
  writeReg(0x70, 128);     // DAC R2 Vol

  writeReg(0x76, 0x0F);     // Enable DACs 0x0f is both
  writeReg(0x78, 0x40);     // Power Up
  Serial.println(readReg(0x76), HEX);  // Expect: 0F
}

void configureTDM() {
  I2S1_TCR4 = I2S_TCR4_FRSZ(15); // 16 slots
  I2S1_TCR5 = I2S_TCR5_WNW(15) | I2S_TCR5_W0W(15) | I2S_TCR5_FBT(15);
  I2S1_TCR2 = I2S_TCR2_SYNC(1);
}

void setup() {
  delay(10000);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.begin(115200);
  Wire.begin();
  delay(1000);

  Serial.println(" Start Audio Library");
  AudioMemory(320);
  delay(1000);

  sine1.frequency(440);
  sine1.amplitude(0.8);

  sine2.frequency(240);
  sine2.amplitude(0.8);

  Serial.println(" Dump Registers");
  dumpRegisters(64);
}

void loop() {
  //static const uint8_t cfgs[] = {0x00, 0x04, 0x10, 0x14};
  static const uint8_t cfgs[] = {0x00};
  static bool pllMode = false;
  static uint8_t idx = 0;

  // Serial.println();
  // Serial.print(pllMode ? "🧪 PLL Mode — " : "🔬 External Clk — ");
  // Serial.print("Trying PASI_CFG0 = 0x"); Serial.println(cfgs[idx], HEX);

  uint8_t clk2 = readReg(0x13);
if ((clk2 & 0x07) == 0x07) {
  Serial.println("✅ Clocks locked");
} else {
  Serial.println("❌ Clocks not locked");
}

  setupCodec(pllMode, cfgs[idx]);
  //configureTDM();
  //delay(300);

  uint8_t clk = readReg(0x13);
  Serial.print("⏱ Clock Status (0x13): 0x"); Serial.println(clk, HEX);

  if (clk & 0x07) {
    Serial.println("✅ CLOCKS LOCKED!");
    digitalWrite(LED_BUILTIN, HIGH);
    while (true) {
      digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
      delay(500);
    }
  }

  // idx++;
  // if (idx >= sizeof(cfgs)) {
  //   idx = 0;
  //   pllMode = !pllMode;
  //   Serial.println("🔁 Switching clock mode...");
  // }

  delay(2000);
}

Here is the result.

Code:
Start Audio Library
 Dump Registers
🧾 Dumping first codec registers:
0x00: 0x00
0x01: 0x00
0x02: 0x09
0x03: 0x00
0x04: 0x00
0x05: 0x15
0x06: 0x35
0x07: 0x00
0x08: 0x00
0x09: 0x00
0x0A: 0x32
0x0B: 0x00
0x0C: 0x00
0x0D: 0x00
0x0E: 0x00
0x0F: 0x00
0x10: 0x52
0x11: 0x80
0x12: 0x00
0x13: 0x00
0x14: 0x00
0x15: 0x00
0x16: 0x00
0x17: 0x00
0x18: 0x40
0x19: 0x12
0x1A: 0x30
0x1B: 0x00
0x1C: 0x00
0x1D: 0x00
0x1E: 0x20
0x1F: 0x21
0x20: 0x02
0x21: 0x03
0x22: 0x04
0x23: 0x05
0x24: 0x06
0x25: 0x07
0x26: 0x00
0x27: 0x00
0x28: 0x20
0x29: 0x21
0x2A: 0x02
0x2B: 0x03
0x2C: 0x04
0x2D: 0x05
0x2E: 0x06
0x2F: 0x07
0x30: 0x00
0x31: 0x00
0x32: 0x00
0x33: 0x00
0x34: 0x40
0x35: 0x00
0x36: 0x00
0x37: 0x20
0x38: 0x00
0x39: 0x00
0x3A: 0x00
0x3B: 0x00
0x3C: 0x00
0x3D: 0x10
0x3E: 0x50
0x3F: 0x00
❌ Clocks not locked
🎧 Using External Clock Mode
F
⏱ Clock Status (0x13): 0x0
❌ Clocks not locked
🎧 Using External Clock Mode
F
⏱ Clock Status (0x13): 0x0
❌ Clocks not locked
🎧 Using External Clock Mode
F



If you’ve worked with TDM audio on Teensy or similar codecs, I’d really value your insight on what to try next—especially around clock settings and initialization. While I’ve been working on audio and embedded projects for years, I’m still early in this particular journey and have big goals for where I’d like to take it. If you're into Teensy, TDM, modular audio, or just love to tinker—let’s connect.

I’ll be sharing more details soon, including the full backplane schematic, expansion plans, and some of the other modules I’m working on.

Thanks in advance for your help!

Best,
Jay Shoemaker
t-dsp.com
 
Last edited:
I'm not super happy with the way the SD line looks. I didn't think I needed a 50-100 ohm resistor on that line but maybe it should have it to clean it up. The clock lines currently have 100 ohm resistors close to the Teensy - not the OUT1A.

The frame-sync and bit-clock lines fall together. I need to check with the datasheet to see if that's expected or inversed. I can inverse the Teensy, or this chip has an inverse option in the register.

It doesn't appear that the SD slot 0 has any offset.

PXL_20250405_115822773 (1).jpg


PXL_20250405_115416883.jpg


PXL_20250405_115434790.jpg


PXL_20250405_115458850.jpg
 
The top scope image looks good - any ringing is due to your long ground clips and that long blue ground extension wire - lots of inductance like that will make any logic signal look like its ringing when it probably isn't. For some reason the bandwidth of the cyan traces is very different between top and middle 'scope images - the middle image is definitely too bandwidth limited to be reliable I would have thought.
 
Hello MarkT,

Thanks for your response. I originally thought the clocks looked fine as well. I'm confident in the FSYNC and BCLK signals, though the SD line is probed with a lower quality probe so it may appear less clean in the captures. I intentionally zoomed out to show the overall signal pattern, even if it's not a perfect edge capture.

I've spent weeks trying to get the TI TAC5212 codec working via TDM with a Teensy 4.1 using the Audio library. I’ve gotten it almost working—there is sound output, but it is consistently scratchy or distorted.

I've also tested I2S mode with very similar results: functional but scratchy. Interestingly, some TDM configurations sound cleaner than others, but never completely clean. I'm wondering if the issue lies in slot alignment or edge timing. I’ve tried multiple clock modes, including BCLK-driven and MCLK auto mode.

Key Observations:​

  • When FSYNC rises, the TAC5212 documentation defines that moment as the start of the frame.
  • The Teensy, however, seems to start data transmission when FSYNC falls, and this falling edge coincides with the falling edge of BCLK.
  • That would imply PASI_BCLK_POL = 1 (falling edge) and possibly PASI_FSYNC_POL = 0 (active low), according to the codec datasheet.
  • My scope shows data on the SD line starting immediately after FSYNC and BCLK fall, suggesting an RX offset of 1 BCLK cycle.

Here is an image of the mode I believe the Teensy is sending, just that PASI_RX_OFFSET = 1.


1743940083555.png


Here’s the current register setup I believe best matches the Teensy TDM signal:

C++:
  // --- PASI_CFG0 (0x1A): Protocol + Word Length + Polarity ---
  uint8_t PASI_FORMAT        = 0b00 << 6; // [7:6] PASI_FORMAT: 00 = TDM, 01 = I2S, 10 = Left Justified
  uint8_t PASI_WLEN          = 0b00 << 4; // [5:4] PASI_WLEN: 00 = 16-bit, 01 = 20-bit, 10 = 24-bit, 11 = 32-bit
  uint8_t PASI_FSYNC_POL     = 0 << 3;    // [3]   FSYNC_POL: 0 = Active Low, 1 = Active High
  uint8_t PASI_BCLK_POL      = 1 << 2;    // [2]   BCLK_POL:  0 = Normal (rising edge), 1 = Inverted (falling edge)
  uint8_t PASI_BUS_ERR       = 0 << 1;    // [1]   BUS_ERR:   0 = Disable error detection, 1 = Enable
  uint8_t PASI_BUS_ERR_RCOV  = 1 << 0;    // [0]   BUS_ERR_RCOV: 0 = Disable recovery, 1 = Enable
  uint8_t pasi_cfg0 = PASI_FORMAT | PASI_WLEN | PASI_FSYNC_POL | PASI_BCLK_POL | PASI_BUS_ERR | PASI_BUS_ERR_RCOV;
  writeReg(0x1A, pasi_cfg0);            // Final value
 
    // --- PASI_RX_CFG0 (0x26): Input clocking + slot offset ---
  uint8_t PASI_RX_EDGE           = 0 << 7; // [7]   PASI_RX_EDGE: 0 = Normal edge, 1 = Half-cycle delay
  uint8_t PASI_RX_USE_INT_FSYNC  = 0 << 6; // [6]   USE_INT_FSYNC: 0 = External FSYNC, 1 = Internal
  uint8_t PASI_RX_USE_INT_BCLK   = 0 << 5; // [5]   USE_INT_BCLK: 0 = External BCLK, 1 = Internal
  uint8_t PASI_RX_OFFSET         = 1;      // [4:0] PASI_RX_OFFSET: Shift by 0–31 BCLKs
  uint8_t pasi_rx_cfg0 = PASI_RX_EDGE | PASI_RX_USE_INT_FSYNC | PASI_RX_USE_INT_BCLK | PASI_RX_OFFSET;
  writeReg(0x26, pasi_rx_cfg0);             // Final value


There is an Application Note: Clocking Configuration of Device and Flexible Clocking For TAx5x1x Family which specifies applicable ranges for clock detection.

Clock Info​

  • Teensy BCLK = 12.288 MHz
  • FSYNC = 24 kHz
  • BCLK/FSYNC Ratio = 512 (standard TDM)
  • MCLK = 24.576 MHz connected to GPI1
The codec should support these values according to:
  • Table 1-1 / 1-2 (FSYNC range)
  • Table 2-2 (512 BCLK:FSYNC ratio)
I’ve also attempted MCLK Auto and MCLK Fixed modes (with PLL enabled), but in all of those cases I get no sound, not scratchy just silence. Also, the codec doesn’t lock at all in those modes. I'm happy to try again if I had a better configuration in mind.

I've posted on the TI forum and they are usually pretty good about answering during regular business hours. I should hear from them next week.

I’m running out of ideas. If anyone here has any other ideas, I’d really appreciate your insights.

Thanks in advance!
 
Good morning,

I received a response from TI, and they’re going to investigate the issue further. Their first question was whether my host device (Teensy) is capable of modifying the TX_OFFSET so that it begins transmitting data immediately, without the 1-bit offset.

Could someone help me locate where in the Teensy Audio Library I can modify the code to set TX_OFFSET = 0? I'm also interested in understanding where the clock setup is defined in general. I’m considering reversing the current polarity of the BCLK line as well, so any guidance on that would be appreciated.

He also confirmed that I don’t need MCLK, which I had already suspected.

Thanks,
Jay
 
Hello Pio,

That’s interesting. Just to confirm, are you saying the Teensy is supposed to assert Fsync at the exact moment the first data bit is transmitted? And by enabling “Frame Sync Early,” it shifts Fsync one bit earlier to compensate for what appears to be a one-bit delay? I wonder if there is a setting that is pushing the data forward - and should be set to NOT do that - instead of a setting that pulls the data back.

I tried applying this setting by adding the following to the setup code. I’ve found that I may need to do a clean build each time I change this setting. When I enabled it, I did notice a change in the audio, specifically a high-pitched squeal, but the clocks appeared unchanged on the scope. I reversed the change and did a full clean rebuild, which restored proper audio. I tested with and without the commented lines, cleaning between each build, but had no success getting it to behave as expected.

Code:
void configureI2STransmitFrame_TAS5212() {
  // Set:
  // - MSB First
  // - Frame Sync Early
  // - 2 Words Per Frame (Stereo)
  // - 16-bit Sync Width (1 word)
  I2S1_TCR4 = 
      // I2S_TCR4_MF |             // MSB First
      I2S_TCR4_FSE |            // Frame Sync Early
      // I2S_TCR4_FRSZ(1) |        // 2 words per frame
      // I2S_TCR4_SYWD(15);        // 16-bit sync width
  Serial.printf("I2S1_TCR4 (TAS5212 mode): 0x%08X\n", I2S1_TCR4);
}

Also, I might roll out another prototype of the T-DSP TAC5212 Pro Audio Modules soon, possibly along with a revision of the backplane. In the meantime, I have a few prototypes on hand and can send them out to anyone interested in testing. The main contributors to the audio library will be eligible for a free shipment of the final build. You know who you are.

Jay
 
1 bit delay is specified in the I2S format. MSB justified mode is similar, but has no delay.
FSE should be set to 0 to remove the delay. Looking at the code for the TDM output, that bit it set by default, same for the receiver side.

What i would do is as usually, approach the issues step by step:
- scope the bus using the springy GND contancs instead of long wire, as MarkT mentioned. Make sure there are no signal intergrity problems like slow edges, crosstalk etc.
- If all looks good, switch to the logic analyzer + data decoder. Seeing the numerical values for a known waveform (i tend to use a ramp/sawtooth in such cases) often helps to see where the data corruption happens.
If reconfiguring the TDM/I2S on the Teensy, doing it before the codec init is a better idea. I see you have it the the other way around in the code posted above. Have the TDM bus output all the clocks and then reset & configure the codec.
Dataheet for the MCU, section 38.5.1.8 Transmit Configuration 4 (TCR4) says
NOTE
This register must not be altered when TCSR[TE] is set.
Reconfiguring the TCRx registers once the TDM is enabled and running is not allowed or has no effect. This coud explain why there is no change visible on the scope traces. Disabling the TCSR[TE] before doing the changes might help.
 
Back
Top