Help bringing up WM8904 on Teensy 4.1

KyleA912

Member
Hello all,

I am trying to get the WM8904 audio codec eval board (AC328904) working with a Teensy 4.1, but I can't seem to get any signs of life from the ADCs or DACs.

I can communicate over I2C just fine (set and read back config registers correctly). I probed the I2S lines and see the Teensy correctly giving the MCLK, BCLK, and LRCLK signals to the WM8904, but there's no activity on ADCDAT or DACDAT. Also, I try putting the CODEC in loopback mode, but don't see anything on either DAC when inputting a 1kHz 0.5Vpk sine wave onto both ADC inputs.

Any ideas on what could be happening? Here's my test code:


Code:
#include <Arduino.h>
#include <Audio.h>
#include "Wire.h"
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

#define LED_PIN 13
#define BUTTON 12
#define WM8904_I2C_ADDRESS 0x1A //0x34

#define SDCARD_CS_PIN    BUILTIN_SDCARD
#define SDCARD_MOSI_PIN  11  // not actually used
#define SDCARD_SCK_PIN   13  // not actually used

//I2S Setup
AudioInputI2S            i2s2;           //xy=105,63
AudioAnalyzePeak         peak1;          //xy=278,108
AudioRecordQueue         queue1;         //xy=281,63
AudioPlaySdRaw           playRaw1;       //xy=302,157
AudioOutputI2S           i2s1;           //xy=470,120
AudioConnection          patchCord1(i2s2, 0, queue1, 0);
AudioConnection          patchCord2(i2s2, 0, peak1, 0);
AudioConnection          patchCord3(playRaw1, 0, i2s1, 0);
AudioConnection          patchCord4(playRaw1, 0, i2s1, 1);


// The file where data is recorded
File frec;

bool recordingActive = false;
bool loopbackMode = true;

void setup()
{
  Serial.begin(9600);

  // Audio connections require memory, and the record queue
  // uses this memory to buffer incoming audio.
  AudioMemory(60);

  Wire2.begin();

  //Enable CODEC
  CodecEnable();

  //Check CODEC ID
  uint16_t codecId = 0;
  CodecRead(0, &codecId);
  Serial.print("Codec ID: ");
  Serial.println(codecId, HEX);


  // Initialize the SD card
  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here if no SD card, but print a message
    while (1) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }

 
  if (loopbackMode)
  {
    //Put CODEC in loopback mode
    uint16_t loopbackReg = 0;
    CodecRead(0x18, &loopbackReg);
    loopbackReg = (loopbackReg & 0xFEFF) + (1 << 8);
    CodecWrite(0x18, loopbackReg);
  }
  else
  {
    //Log ADC data to SD card
    StartRecording();
  }
}


void loop()
{
  if (recordingActive)
  {
    ContinueRecording();
  }
}


void CodecEnable()
{
  //reset
  Serial.println("Resetting");
  CodecWrite(0x00, 0x0000);
  delay(10);

  //Automatic startup sequence
  Serial.println("Beginning auto startup sequence");
  CodecWrite(0x6F, 0x0100);
  delay(500);

  //Enable ADCs and DACs
  Serial.println("Enabling ADCs and DACs");
  CodecWrite(0x12, 0x000F);
  delay(10);
}


int CodecWrite(uint8_t reg, uint16_t value)
{
  uint8_t data[3];
  data[0] = reg;  // Register address
  data[1] = (value >> 8) & 0xFF; // Data MSB
  data[2] = value & 0xFF;       // Data LSB

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(data, (uint8_t)3);
  return Wire2.endTransmission();
}


int CodecRead(uint8_t reg, uint16_t *value)
{
  uint8_t data[10];

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(reg);
  Wire2.endTransmission();

  Wire2.requestFrom(WM8904_I2C_ADDRESS, 2);
  data[0] = Wire2.read();
  data[1] = Wire2.read();

  *value = (data[0] << 8) | data[1];
  return 0;
}


void StartRecording()
{
  /*
  Serial.println("Starting recording");

  //Delete old file if present
  if (SD.exsists("RECORD.RAW"))
  {
    SD.remove("RECORD.RAW");
  }
  frec = SD.open("RECORD.RAW", FILE_WRITE);

  if (frec)
  {
    queue1.begin();
    recordingActive = true;
  }
  */
  queue1.begin();
  recordingActive = true;
}


void ContinueRecording()
{
  if (queue1.available() >= 2)
  {
    byte buffer[512];
    // Fetch 2 blocks from the audio library and copy
    // into a 512 byte buffer.  The Arduino SD library
    // is most efficient when full 512 byte sector size
    // writes are used.
    memcpy(buffer, queue1.readBuffer(), 256);
    queue1.freeBuffer();
    memcpy(buffer+256, queue1.readBuffer(), 256);
    queue1.freeBuffer();
    // write all 512 bytes to the SD card
    //elapsedMicros usec = 0;
    //frec.write(buffer, 512);
    
    
    Serial.print(buffer[0]);
    Serial.print(",");
    Serial.print(buffer[1]);
    Serial.print(",");
    Serial.println(buffer[2]);
    
  }
}
 
You might want to start by converting a simpler example, e.g. the PTS8211sine one - doesn’t get much simpler than that.

It seems loopback just sends your digital output back to the input. None of your code generates output, and the signal input will be ignored, so seeing nothing isn’t surprising.

Well done for finding the code button, so many don’t and it makes posted code hard to read and can even introduce apparent bugs. However, there’s a lot left in that’s irrelevant, like commented-out code, there’s clearly missing code (e.g. a way of stopping recording), all of which makes it much harder for the reader to spot errors.
 
Thank you for the feedback. Sorry, I should've specified that I am mainly trying to evaluate the ADCs at the moment. The relevant code here was converted from the recorder example, which seemed like a pretty straight forward example to work from. Especially since it also includes logging to the uSD, which I intend to do.

I was also trying the loopback mode to verify the signal I injected into the ADCs was being received successfully. That's working under the assumption that the loopback sends ADC data to the DAC, which the datasheet seems to support, vs. DAC data to ADC as you're suggesting. Here's the loopback section from the datasheet-

Screenshot 2025-04-03 153606.png


And very true, I don't have a function to stop the recording yet. I am happy to update my example code to include that as well as omit the section of commented out code. It's only missing because I've already encountered the issue I'm posting about before getting to that point of the code, and I imagined that not including more code than necessary was preferable (not leaving in irrelevant bits like you mention).
 
Not a major issue, just making the point (badly) that the essence of debugging tends to be to strip things back to the bare minimum, so it’s obvious to all that it should work … just that it doesn’t. Often that results in your finding out where you went wrong, of course :)

The fact remains that nothing in the code you posted generates output data, so the codec has nothing to loop back, so you won’t see “activity on ADCDAT or DACDAT”. If you adapt the PTS8211sine example by adding your initialisation code, you should certainly see data output from the Teensy, and either hear it output from the codec or see it looped back, depending.

Recording audio to SD is a subject about which you (or, let’s face it, Google) will find a lot of discussion on this forum. Do bear in mind some of the examples you’ll find are a bit past their sell-by date, or at least fit only to show the possibility exists, rather than the basis of a useful application. I’ve put in a lot of effort myself, and hope that it will get into the official Teensyduino package soon. There’s a forum thread here.
 
I'm not sure I'm following you on the loopback... I am not trying to output any digitally generated waveforms, so I shouldn't need any digitally generated signals in my code. It should send the signal received on the ADC directly to the DAC, and I'm putting a 1kHz 0.5Vpk test signal on the ADC, so that's what I'm expecting to see on the DAC. It's just setting a hardware register in the CODEC so theoretically I shouldn't need any Teensy interaction to the I2S interface, just setting that bit over the I2C interface.
And even without a signal on the ADC, if the CODEC is working correctly then I should at least see some LSBs toggle just from noise when probing the ADCDAT I2S line...

Actually, for just the loopback test, I would expect even the following code to work (cleaned up a bit more :))-

Code:
#include "Wire.h"

#define WM8904_I2C_ADDRESS 0x1A

void setup()
{
  Serial.begin(9600);

  Wire2.begin();

  //Enable CODEC (trigger default config procedure in HW)
  CodecEnable();

  //Check CODEC ID
  uint16_t codecId = 0;
  CodecRead(0, &codecId);
  Serial.print("Codec ID: ");
  Serial.println(codecId, HEX);
 
  //Put CODEC in loopback mode
  uint16_t loopbackReg = 0;
  CodecRead(0x18, &loopbackReg);
  loopbackReg = (loopbackReg & 0xFEFF) + (1 << 8);
  CodecWrite(0x18, loopbackReg);
}


void loop()
{
}


void CodecEnable()
{
  //reset
  Serial.println("Resetting");
  CodecWrite(0x00, 0x0000);
  delay(10);

  //Automatic startup sequence
  Serial.println("Beginning auto startup sequence");
  CodecWrite(0x6F, 0x0100);
  delay(500);

  //Enable both ADCs and DACs (only DACs enabled in default startup sequence)
  Serial.println("Enabling ADCs and DACs");
  CodecWrite(0x12, 0x000F);
  delay(10);
}


int CodecWrite(uint8_t reg, uint16_t value)
{
  uint8_t data[3];
  data[0] = reg;  // Register address
  data[1] = (value >> 8) & 0xFF; // Data MSB
  data[2] = value & 0xFF;       // Data LSB

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(data, (uint8_t)3);
  return Wire2.endTransmission();
}


int CodecRead(uint8_t reg, uint16_t *value)
{
  uint8_t data[10];

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(reg);
  Wire2.endTransmission();

  Wire2.requestFrom(WM8904_I2C_ADDRESS, 2);
  data[0] = Wire2.read();
  data[1] = Wire2.read();

  *value = (data[0] << 8) | data[1];
  return 0;
}

What are your thoughts?

I'll take a look at the PTS8211 sine example code, but frankly I'm initially only interested in evaluating the long term performance of the ADCs before deciding if I even want to move forward using this part, and from a brief look when you mentioned it previously, it looks like that example program is focused on generating a signal on the DAC.
 
Looking at the Datasheet listing the startup seq, i see a bunch of power up commands and clk enabling, the I2S config holds the default values, i guess.
These values are almost correct, except the word length, which is by default set to 24bit
reg 19h:
3:2 AIF_WL [1:0] 10 Digital Audio Interface Word Length
10 - corresponds to 24bit
Teensy needs 32bit, so maybe try setting it to 11.
wm8904-i2s.png
 
Hmmm ... on second reading, you're probably right - it's kind of not a digital loopback at all, it's an analogue loopback that goes through the digital processing. I read it as anything the Teensy sends out digitally is looped back in.

@Pio could well be right. Certainly the Teensy does typically output 32-bit words for each channel (with the LSBs set to 0), so that's worth a try.
 
By default it starts in slave mode, which is also correct. With the ADC data internally routed to DAC it won't work if the externally supplied clock signals (Teensy) do not match the default i2s settings, hence the loopback mode also doesn't work. That's how i understand it.
wm8904-i2s_b.png
 
Nice catch on both fronts @Pio! I appended the configuration of the word length to the end of my startup routine. The issue w/ the clock signals also just occurred to me so I adjusted it to start recording regardless of loopback state so it still receives the clock signals, but unfortunately still no luck with either fix :(

New code-

Code:
#include <Arduino.h>
#include <Audio.h>
#include "Wire.h"
#include <SPI.h>
#include <SerialFlash.h>

#define LED_PIN 13
#define BUTTON 12
#define WM8904_I2C_ADDRESS 0x1A //0x34

//I2S Setup
AudioInputI2S            i2s2;           //xy=105,63
AudioAnalyzePeak         peak1;          //xy=278,108
AudioRecordQueue         queue1;         //xy=281,63
AudioPlaySdRaw           playRaw1;       //xy=302,157
AudioOutputI2S           i2s1;           //xy=470,120
AudioConnection          patchCord1(i2s2, 0, queue1, 0);
AudioConnection          patchCord2(i2s2, 0, peak1, 0);
AudioConnection          patchCord3(playRaw1, 0, i2s1, 0);
AudioConnection          patchCord4(playRaw1, 0, i2s1, 1);

bool recordingActive = false;
bool loopbackMode = true;

void setup()
{
  Serial.begin(9600);

  // Audio connections require memory, and the record queue
  // uses this memory to buffer incoming audio.
  AudioMemory(60);

  Wire2.begin();

  //Enable CODEC
  CodecEnable();

  //Check CODEC ID
  uint16_t codecId = 0;
  CodecRead(0, &codecId);
  Serial.print("Codec ID: ");
  Serial.println(codecId, HEX);

  //Correct word size setting from 24 to 32
  uint16_t audioConfig = 0;
  CodecRead(0x19, &audioConfig);
  audioConfig = (audioConfig & 0xFFF3) + 0b1100;
  CodecWrite(0x19, audioConfig);

 
  if (loopbackMode)
  {
    //Put CODEC in loopback mode
    uint16_t loopbackReg = 0;
    CodecRead(0x18, &loopbackReg);
    loopbackReg = (loopbackReg & 0xFEFF) + (1 << 8);
    CodecWrite(0x18, loopbackReg);
  }

  //Log ADC data to SD card
  StartRecording();
}


void loop()
{
  if (recordingActive)
  {
    ContinueRecording();
  }
}


void CodecEnable()
{
  //reset
  Serial.println("Resetting");
  CodecWrite(0x00, 0x0000);
  delay(10);

  //Automatic startup sequence
  Serial.println("Beginning auto startup sequence");
  CodecWrite(0x6F, 0x0100);
  delay(500);

  //Enable ADCs and DACs
  Serial.println("Enabling ADCs and DACs");
  CodecWrite(0x12, 0x000F);
  delay(10);
}


int CodecWrite(uint8_t reg, uint16_t value)
{
  uint8_t data[3];
  data[0] = reg;  // Register address
  data[1] = (value >> 8) & 0xFF; // Data MSB
  data[2] = value & 0xFF;       // Data LSB

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(data, (uint8_t)3);
  return Wire2.endTransmission();
}


int CodecRead(uint8_t reg, uint16_t *value)
{
  uint8_t data[10];

  Wire2.beginTransmission(WM8904_I2C_ADDRESS);
  Wire2.write(reg);
  Wire2.endTransmission();

  Wire2.requestFrom(WM8904_I2C_ADDRESS, 2);
  data[0] = Wire2.read();
  data[1] = Wire2.read();

  *value = (data[0] << 8) | data[1];
  return 0;
}


void StartRecording()
{
  queue1.begin();
  recordingActive = true;
}


void ContinueRecording()
{
  if (queue1.available() >= 2)
  {
    byte buffer[512];
    // Fetch 2 blocks from the audio library and copy
    // into a 512 byte buffer.  The Arduino SD library
    // is most efficient when full 512 byte sector size
    // writes are used.
    memcpy(buffer, queue1.readBuffer(), 256);
    queue1.freeBuffer();
    memcpy(buffer+256, queue1.readBuffer(), 256);
    queue1.freeBuffer();   
    
    //Read first few indexes of buffer to verify data is being received
    Serial.print(buffer[0]);
    Serial.print(",");
    Serial.print(buffer[1]);
    Serial.print(",");
    Serial.println(buffer[2]);
    
  }
}
 
I'm finally getting back to this and just wanted to update on some new progress-
I used a simpler CODEC (CS5343 on digilent PMOD I2S2 board) to test the I2S code above and it is working correctly, which to me reinforces the idea that I'm probably just not correctly configuring the WM8904 registers.
I was going to continue troubleshooting, but I just caught (far too late) that the Audio library truncates all data to 16 bits, which is unfortunately not acceptable for my application (not strictly audio, but resonant sensors which operate in the audio frequency range). With that being the case, I'm likely going to continue the effort on an FPGA where I already have similar code that I can adapt. Although I will still use the Teensy assembly out of convenience to finish troubleshooting the I2C config register issue. I'll update this thread when I find the resolution for anyone who might have my same problem in the future.

Please correct me if my info on the Audio library truncating to 16 bits is outdated. I did some digging and everywhere I saw states that it's 16 bits along with some spirited discussion on the value in 24 bit audio data, but it's all from pretty old forums. I did see Paul mention in a post several years ago that expanding the bit depth might find its way onto his to do list in the following year, but haven't found any follow up on that.

Thank you guys for the help :)
 
Back
Top