Thank you for your response! I’m a big fan. I’ve worked with your 9DOF Magnetometer ICM_20948-AHRS project, and learned a lot from it!
Full code posted below. Some notes:
- If #define INLINE_PRINT is commented out, it’s the Sparkfun Example code Example_08_I2S_Passthrough for the WM8906 almost exactly (had to replace I2S_MCLK_MULTIPLE_DEFAULT with I2S_MCLK_MULTIPLE_512 to make it compile).
- yes, I’m using a negative voltage. That’s the reason for using the WM8906: it handles audio line input and line output. Two pieces of evidence this is working: 1)audio attached to the ADC comes back as audio out the DAC, and 2) Listening to the clear tone and looking at the PulseView Capture of the I2S traffic in both directions decodes as a 300 Hz, +/-6860 is sine wave when the function generator is attached to one of the channels instead of audio feed (see figure). Of course it’s only a clear tone when not doing the INLINE_PRINT.
- when #define INLINE_PRINT is not commented out, it’s not a clear tone of course, but it is an attempt to look at the buffer contents.
Here’s some sample output of the buffer as hex from one run, and decimal as another. Format is array element number (Hex): buffer value (Hex for one run, DEC for the other).
Thanks for taking a look at this.
INLINE_PRINT sbuffer: 0:74DA 1:FFFFFFFF 2:FFFFF48B 3:7FFE 4:FFFFF442 5:7FFF 6:73FD 7:7FFF 8:73BD 9:0 A:7382 B:7FFF C:FFFFF350 D:FFFFFFFF E:7325 F:7FFF 10:7301 11:FFFF8000 12:72E3 13:FFFFFFFF 14:FFFFF2CA 15:7FFF 16:72B6 17:FFFFFFFF 18:72A9 19:FFFFFFFF 1A:72A0 1B:FFFF8000 1C:729D 1D:7FFF 1E:72A4 1F:7FFF 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:7C5C 1:FFFF8000 2:7CEB 3:0 4:FFFFFD7A 5:FFFF8000 6:7E0C 7:7FFF 8:FFFFFE9C 9:FFFF8001 A:7F2E B:7FFF C:FFFFFFBE D:FFFF8000 E:FFFF804E F:FFFF8000 10:E1 11:7FFF 12:FFFF8173 13:FFFF8000 14:207 15:FFFF8000 16:FFFF8297 17:0 18:FFFF8326 19:FFFF8000 1A:FFFF83B5 1B:FFFF8000 1C:43F 1D:FFFFFFFF 1E:FFFF84C9 1F:FFFF8000 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:FFFFFFC2 1:7FFF 2:54 3:0 4:E5 5:0 6:175 7:0 8:FFFF8205 9:FFFF8000 A:298 B:0 C:FFFF8326 D:FFFF8000 E:3B6 F:1 10:444 11:0 12:FFFF84CE 13:0 14:556 15:0 16:5DB 17:FFFF8000 18:FFFF865C 19:0 1A:6DC 1B:FFFF8000 1C:FFFF8757 1D:FFFFFFFF 1E:FFFF87CF 1F:FFFFFFFF 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:325 1:FFFF8000 2:3B2 3:FFFF8000 4:440 5:FFFF8000 6:FFFF84C9 7:FFFF8000 8:553 9:FFFFFFFF A:FFFF85D9 B:FFFF8000 C:65D D:0 E:FFFF86DD F:0 10:75A 11:0 12:7D0 13:FFFFFFFF 14:FFFF8844 15:FFFF8000 16:FFFF88B3 17:0 18:920 19:0 1A:987 1B:7FFF 1C:FFFF89EC 1D:FFFF8000 1E:FFFF8A4A 1F:FFFF8000 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:29395 1:-2 2:-3394 3:32767 4:29359 5:-1 6:-3419 7:-1 8:-3426 9:-2 A:-3423 B:32767 C:-3413 D:-1 E:-3396 F:-32768 10:29393 11:-1 12:-3345 13:-1 14:29457 15:0 16:29493 17:-32768 18:29540 19:32767 1A:29594 1B:32767 1C:-3115 1D:32767 1E:-3051 1F:32767 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:-3396 1:-32768 2:29394 3:32767 4:29423 5:-1 6:-3311 7:-1 8:-3271 9:-1 A:-3226 B:-1 C:-3174 D:32767 E:-3117 F:-32768 10:29716 11:-1 12:-2983 13:-1 14:-2909 15:0 16:29941 17:32767 18:-2745 19:-1 1A:-2655 1B:32767 1C:-2558 1D:-32768 1E:30311 1F:-1 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:3020 1:-1 2:-29821 3:-1 4:-29896 5:32767 6:-29980 7:-32768 8:2698 9:-1 A:-30160 B:-32768 C:2513 D:32767 E:-30357 F:-32768 10:-30463 11:32767 12:-30572 13:-1 14:2081 15:-1 16:-30806 17:32767 18:-30927 19:0 1A:1715 1B:0 1C:1587 1D:0 1E:1456 1F:0 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
INLINE_PRINT sbuffer: 0:43 1:32767 2:-103 3:0 4:-248 5:-32768 6:32375 7:-1 8:-536 9:0 A:32089 B:0 C:31946 D:0 E:-961 F:0 10:31665 11:0 12:31528 13:32767 14:-1376 15:0 16:31258 17:-32768 18:31129 19:32767 1A:31001 1B:0 1C:-1891 1D:-1 1E:-2014 1F:32767 20:0 21:0 22:0 23:0 24:0 25:0 26:0 27:0 28:0 29:0 2A:0 2B:0 2C:0 2D:0 2E:0 2F:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 39:0 3A:0 3B:0 3C:0 3D:0 3E:0 3F:0
and the full sketch:
/******************************************************************************
Example_08_I2S_Passthrough.ino
Demonstrates reading I2S audio from the ADC, and passing that back to the DAC.
This example sets up analog audio input (on INPUT1s), ADC/DAC enabled as I2S
peripheral, sets volume control, and Headphone output on the WM8960 Codec.
Audio should be connected to both the left and right "INPUT1" inputs,
they are labeled "RIN1" and "LIN1" on the board.
This example will pass your audio source through the mixers and gain stages of
the codec into the ADC. Read the audio from the ADC via I2S. Then send audio
immediately back to the DAC via I2S. Then send the output of the DAC to the
headphone outs.
Development platform used:
SparkFun ESP32 IoT RedBoard v10
HARDWARE CONNECTIONS
**********************
ESP32 ------- CODEC
**********************
QWIIC ------- QWIIC *Note this connects GND/3.3V/SDA/SCL
GND --------- GND *optional, but not a bad idea
5V ---------- VIN *needed to power codec's onboard AVDD (3.3V vreg)
4 ----------- DDT *aka DAC_DATA/I2S_SDO/"serial data out", this carries the I2S audio data from ESP32 to codec DAC
16 ---------- BCK *aka BCLK/I2S_SCK/"bit clock", this is the clock for I2S audio, can be controlled via controller or peripheral.
17 ---------- ADAT *aka ADC_DATA/I2S_SD/"serial data in", this carries the I2S audio data from codec's ADC to ESP32 I2S bus.
25 ---------- DLRC *aka I2S_WS/LRC/"word select"/"left-right-channel", this toggles for left or right channel data.
25 ---------- ALR *for this example WS is shared for both the ADC WS (ALR) and the DAC WS (DLRC)
**********************
CODEC ------- AUDIO IN
**********************
GND --------- TRS INPUT SLEEVE *ground for line level input
LINPUT1 ----- TRS INPUT TIP *left audio
RINPUT1 ----- TRS INPUT RING1 *right audio
**********************
CODEC -------- AUDIO OUT
**********************
OUT3 --------- TRS OUTPUT SLEEVE *buffered "vmid" (aka "HP GND")
HPL ---------- TRS OUTPUT TIP *left HP output
HPR ---------- TRS OUTPUT RING1 *right HP output
You can now control the volume of the codecs built in headphone amp using this
fuction:
codec.setHeadphoneVolumeDB(6.00); Valid inputs are -74.00 (MUTE) up to +6.00,
(1.00dB steps).
Pete Lewis @ SparkFun Electronics
October 14th, 2022
https://github.com/sparkfun/SparkFun_WM8960_Arduino_Library
This code was created using some code by Mike Grusin at SparkFun Electronics
Included with the LilyPad MP3 example code found here:
Revision history: version 1.0 2012/07/24 MDG Initial release
https://github.com/sparkfun/LilyPad_MP3_Player
This code was created using some modified code from DroneBot Workshop.
Specifically, the I2S configuration setup was super helpful to get I2S working.
This example has a similar I2S config to what we are using here: Microphone to
serial plotter example. Although, here we are doing a full duplex I2S port, in
order to do reads and writes. To see the original Drone Workshop code and
learn more about I2S in general, please visit:
https://dronebotworkshop.com/esp32-i2s/
Do you like this library? Help support SparkFun. Buy a board!
SparkFun Audio Codec Breakout - WM8960 (QWIIC)
https://www.sparkfun.com/products/21250
All functions return 1 if the read/write was successful, and 0
if there was a communications failure. You can ignore the return value
if you just don't care anymore.
For information on the data sent to and received from the CODEC,
refer to the WM8960 datasheet at:
https://github.com/sparkfun/SparkFun_Audio_Codec_Breakout_WM8960/blob/main/Documents/WM8960_datasheet_v4.2.pdf
This code is released under the [MIT License](http://opensource.org/licenses/MIT).
Please review the LICENSE.md file included with this example. If you have any questions
or concerns with licensing, please contact techsupport@sparkfun.com.
Distributed as-is; no warranty is given.
******************************************************************************/
#include <Wire.h>
#include <SparkFun_WM8960_Arduino_Library.h>
// Click here to get the library: http://librarymanager/All#SparkFun_WM8960
WM8960 codec;
// Include I2S driver
#include <driver/i2s.h>
// Connections to I2S
#define I2S_WS 25
#define I2S_SD 17
#define I2S_SDO 4
#define I2S_SCK 16
// Use I2S Processor 0
#define I2S_PORT I2S_NUM_0
// Define input buffer length
#define bufferLen 64
int16_t sBuffer[bufferLen];
void setup()
{
Serial.begin(115200);
Serial.println("Example 8 - I2S Passthough");
Wire.begin();
if (codec.begin() == false) //Begin communication over I2C
{
Serial.println("The device did not respond. Please check wiring.");
while (1); // Freeze
}
Serial.println("Device is connected properly.");
codec_setup();
// Set up I2S
i2s_install();
i2s_setpin();
i2s_start(I2S_PORT);
}
void loop()
{
// Get I2S data and place in data buffer
size_t bytesIn = 0;
size_t bytesOut = 0;
esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);
if (result == ESP_OK)
{
#define INLINE_PRINT
#ifdef INLINE_PRINT
int ii;
int16_t sBuffer_copy[bufferLen];
// copy buffer just in case it's changing
for(ii=0;ii<bufferLen;ii++){
sBuffer_copy[ii]=sBuffer[ii];
}
// print buffer
Serial.print("INLINE_PRINT sbuffer:");
for(ii=0;ii<bufferLen;ii++){
Serial.print(" ");
Serial.print(ii,HEX);
Serial.print(":");
Serial.print(sBuffer_copy[ii],HEX);
}
Serial.println(" ");
#endif
// Send what we just received back to the codec
esp_err_t result_w = i2s_write(I2S_PORT, &sBuffer, bytesIn, &bytesOut, portMAX_DELAY);
// If there was an I2S write error, let us know on the serial terminal
if (result_w != ESP_OK)
{
Serial.print("I2S write error.");
}
}
// DelayMicroseconds(300); // Only hear to demonstrate how much time you have
// to do things.
// Do not do much in this main loop, or the audio won't pass through correctly.
// With default settings (64 samples in buffer), you can spend up to 300
// microseconds doing something in between passing each buffer of data
// You can tweak the buffer length to get more time if you need it.
// When bufferlength is 64, then you get ~300 microseconds
// When bufferlength is 128, then you get ~600 microseconds
// Note, as you increase bufferlength, then you are increasing latency between
// ADC input to DAC output.
// Latency may or may not be desired, depending on the project.
}
void codec_setup()
{
// General setup needed
codec.enableVREF();
codec.enableVMID();
// Setup signal flow to the ADC
codec.enableLMIC();
codec.enableRMIC();
// Connect from INPUT1 to "n" (aka inverting) inputs of PGAs.
codec.connectLMN1();
codec.connectRMN1();
// Disable mutes on PGA inputs (aka INTPUT1)
codec.disableLINMUTE();
codec.disableRINMUTE();
// Set pga volumes
codec.setLINVOLDB(0.00); // Valid options are -17.25dB to +30dB (0.75dB steps)
codec.setRINVOLDB(0.00); // Valid options are -17.25dB to +30dB (0.75dB steps)
// Set input boosts to get inputs 1 to the boost mixers
codec.setLMICBOOST(WM8960_MIC_BOOST_GAIN_0DB);
codec.setRMICBOOST(WM8960_MIC_BOOST_GAIN_0DB);
// Connect from MIC inputs (aka pga output) to boost mixers
codec.connectLMIC2B();
codec.connectRMIC2B();
// Enable boost mixers
codec.enableAINL();
codec.enableAINR();
// Disconnect LB2LO (booster to output mixer (analog bypass)
// For this example, we are going to pass audio throught the ADC and DAC
codec.disableLB2LO();
codec.disableRB2RO();
// Connect from DAC outputs to output mixer
codec.enableLD2LO();
codec.enableRD2RO();
// Set gainstage between booster mixer and output mixer
// For this loopback example, we are going to keep these as low as they go
codec.setLB2LOVOL(WM8960_OUTPUT_MIXER_GAIN_NEG_21DB);
codec.setRB2ROVOL(WM8960_OUTPUT_MIXER_GAIN_NEG_21DB);
// Enable output mixers
codec.enableLOMIX();
codec.enableROMIX();
// CLOCK STUFF, These settings will get you 44.1KHz sample rate, and class-d
// freq at 705.6kHz
codec.enablePLL(); // Needed for class-d amp clock
codec.setPLLPRESCALE(WM8960_PLLPRESCALE_DIV_2);
codec.setSMD(WM8960_PLL_MODE_FRACTIONAL);
codec.setCLKSEL(WM8960_CLKSEL_PLL);
codec.setSYSCLKDIV(WM8960_SYSCLK_DIV_BY_2);
codec.setBCLKDIV(4);
codec.setDCLKDIV(WM8960_DCLKDIV_16);
codec.setPLLN(7);
codec.setPLLK(0x86, 0xC2, 0x26); // PLLK=86C226h
//codec.setADCDIV(0); // Default is 000 (what we need for 44.1KHz)
//codec.setDACDIV(0); // Default is 000 (what we need for 44.1KHz)
codec.setWL(WM8960_WL_16BIT);
codec.enablePeripheralMode();
//codec.enableMasterMode();
//codec.setALRCGPIO(); // Note, should not be changed while ADC is enabled.
// Enable ADCs and DACs
codec.enableAdcLeft();
codec.enableAdcRight();
codec.enableDacLeft();
codec.enableDacRight();
codec.disableDacMute();
//codec.enableLoopBack(); // Loopback sends ADC data directly into DAC
codec.disableLoopBack();
// Default is "soft mute" on, so we must disable mute to make channels active
codec.disableDacMute();
codec.enableHeadphones();
codec.enableOUT3MIX(); // Provides VMID as buffer for headphone ground
Serial.println("Volume set to +0dB");
codec.setHeadphoneVolumeDB(0.00);
Serial.println("Codec Setup complete. Listen to left/right INPUT1 on Headphone outputs.");
}
void i2s_install() {
// Set up I2S Processor configuration
const i2s_driver_config_t i2s_config = {
.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX),
.sample_rate = 44100,
.bits_per_sample = i2s_bits_per_sample_t(16),
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_MSB),
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = bufferLen,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
// change here to allow the code to compile (I2S_MCLK_MULTIPLE_DEFAULT not defined)
//.mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_DEFAULT),
.mclk_multiple = i2s_mclk_multiple_t(I2S_MCLK_MULTIPLE_512),
.bits_per_chan = i2s_bits_per_chan_t(I2S_BITS_PER_CHAN_DEFAULT)
};
i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
}
void i2s_setpin() {
// Set I2S pin configuration
const i2s_pin_config_t pin_config = {
.mck_io_num = I2S_PIN_NO_CHANGE,
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_SDO,
.data_in_num = I2S_SD
};
i2s_set_pin(I2S_PORT, &pin_config);
}