Teensy3 I2S with DMA

Status
Not open for further replies.
I just opened up my Teensy 3, and I'm hoping to get hacking on I2S promptly. For those who have been working with it a while, how did you produce test audio in the beginning? I'm planning to be reading audio over Ethernet in the long term, but I'm thinking that right now it might be better to just generate a test signal "by hand" and stuff it in the DMA buffer. Ideas or alternatives?
 
I just opened up my Teensy 3, and I'm hoping to get hacking on I2S promptly. For those who have been working with it a while, how did you produce test audio in the beginning? I'm planning to be reading audio over Ethernet in the long term, but I'm thinking that right now it might be better to just generate a test signal "by hand" and stuff it in the DMA buffer. Ideas or alternatives?

Do you mean as an ouput test? His example produces sine waves.
I was just using my phone to play music and tones into it.

I was talking with hughpyle about getting the library fully working but he simply doesn't have the time. I got input kind of working but there is something wrong with the timing and I don't know where to start with that. So I'm going to stick with SPI DACs I guess.

Code:
/*
  I2S & DMA digital audio demonstrator for Teensy 3.0
  Interfaces using Wolfson WM8731 codec.
  
  To use the Mikro proto board (as master): set clock type to I2S_CLOCK_EXTERNAL
  Note: this board doesn't have line-in connections.
      SCK  -> Teensy 9  (I2S0_TX_BCLK)
      MISO -> Teensy 13
      MOSI -> Teensy 3  (I2S0_TXD0).  Can also be switched to pin 22.
      ADCL -> teensy 12?
      DACL -> Teensy 4  (I2S0_TX_FS).  Can also be switched to pin 23 or 25
      SDA  -> Teensy 18 (I2C0_SDA)
      SCL  -> Teensy 19 (I2C0_SCL)
      3.3V -> Teensy 3.3v
      GND  -> Teensy GND

*/


/* Wolfson audio codec controlled by I2C */
/* Library here: https://github.com/hughpyle/machinesalem-arduino-libs/tree/master/WM8731 */
#include <Wire.h>
#include <WM8731.h>


/* I2S digital audio */
//#define I2S_PIN_PATTERN   I2S_TX_PIN_PATTERN_1
#include <i2s.h>
const uint8_t clocktype = I2S_CLOCK_EXTERNAL;

int tic,jj;
byte led=0;
// audio data
int16_t audf, audx, audy, audd,b,inx,iny;
int32_t nnn=0;

void initsinevalue()
{
  audf = 45 + (30 % 48);                                // midi note number
  float f = (440.0 / 32) * pow(2, ((float)audf - 9) / 12);  // Hz.  For realz, use a lookup table.
  audd = 2.0 * sin(PI*f/48000) * 32767;                     // delta (q15_t)
  audx = 0;
  audy = 0.9 * 32767;                                       // start somewhere near full-scale
}

void nextsinevalue() 
{
  nnn++;
//  if(nnn>48000) {nnn=0;initsinevalue();};                                // reset every second
//  if(nnn>24000){nnn=0;audx=audx<<1;if(audx==0)audx=1;b=audx;};return;  // marching blip
 // audx+=4;if(nnn>512){nnn=0;audx=-2048;};b=audx;return;                // stair
//  b = 0xACCF0010; audx=0xACCF; return;                                 // const pattern
  audx+=((audd*audy)>>15)&0xFFFFu; audy-=((audd*audx)>>15)&0xFFFFu;      // sinewaves http://cabezal.com/misc/minsky-circles.html
}



/* --------------------- Direct I2S data transfer, we get callback to put 2 words into the FIFO ----- */

void i2s_tx_callback( int16_t *pBuf )
{
  pBuf[0] = 0;
  pBuf[1] = inx;
  nextsinevalue() ;
}

void i2s_rx_callback( int16_t *pBuf )
{
inx=  pBuf[0];
iny=  pBuf[1];


  jj++;

}


/* ----------------------- begin -------------------- */

void setup()
{
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  
  initsinevalue();

  unsigned char interface = WM8731_INTERFACE_FORMAT(I2S) | WM8731_INTERFACE_WORDLEN(bits16) | WM8731_INTERFACE_MASTER;
  WM8731.begin( low, WM8731_SAMPLING_RATE(hz48000), interface );
  WM8731.setActive();
  WM8731.setOutputVolume( 0 );
  WM8731.set( WM8731_ANALOG, WM8731_ANALOG_DACSEL);
    delay(1000);
  
  I2STx0.begin(clocktype,  i2s_tx_callback );
  
  I2STx0.start();
    delay(1000);
    
      I2SRx0.begin(clocktype,  i2s_rx_callback );
  
  I2SRx0.start();
    delay(1000);

//  digitalWrite(13,1);
  WM8731.setInputVolume( 31 );

  WM8731.setOutputVolume( 127 );

}


/* --------------------- main loop ------------------ */
void loop()
{
  
  
  tic++;
  if (tic>50000){
        Serial.print(jj);        Serial.print(" ");
        Serial.print(iny);        Serial.print(" ");
    Serial.println(inx);

    tic=0;
}

}
 
I've just posted an update that fixes various things, implements DMA for Receive, and adds a couple "play-through" examples (one with DMA, one without) that listen for audio on the input and play it straight through the output.

The trick to using Tx and Rx simultaneously is to synchronize the Receive channel to Tx. That's hard-wired right now, so I don't know whether Rx will work if Tx is not active. Later...

You still need to patch the Teensy mk20dx128.h header to add various DMA constants. If anyone would prefer I put these in the library header, let me know.

My time on this project has become extremely limited the last couple months, but it's good to be back for a few hours making progress. I hope folks out there find it useful and get some cool audio toys working :)
 
Thanks a lot, hpyle, really appreciated what you have done! For the first time I can play with waveforms and audio math and this is really great, a brand new world to explore!
I think it's better to put the header in the library since paul is so sweet to update (and improve) constantly the software it's easy to get lost. I personally avoid to touch the core files especially when I have a lot of open projects laying around.
But even better, why not integrate header mods in the next teensy update if it's necessary to the I2S? Or it breaks something else?
 
thanks indeed. helped me indefinitely with this little toy -


9204766761_6fc16fb842_b.jpg


... now i have to sit down and write some decent sounding code for it.
 
Hi everyone,

I plan to use the T3 as a I2C master in an audio DAC. I2S would be a great addition to the project, so I'm trying to understand the I2S tx-with-dma thing. Today I modified the example sketch and I'd like to post it here for two reasons. First, please check if my comments are correct. Second, if they are correct, I believe this sketch is a good example to learn from, because it concentrates on the i2s-topic only. I hope the rather verbose comments make it easier to understand for newbies like me :rolleyes:

So here is the sketch, some parts are shamelessly copied from the example on github :p

Code:
/*
  I2S & DMA digital audio demonstrator for Teensy 3.0
      SCK  -> Teensy 9  (I2S0_TX_BCLK)
      MOSI -> Teensy 3  (I2S0_TXD0).  Can also be switched to pin 22.
      DACL -> Teensy 4  (I2S0_TX_FS).  Can also be switched to pin 23 or 25
      3.3V -> Teensy 3.3v
      GND  -> Teensy GND
*/

/* Settings for I2S Interface */
#define clock_per_sec               44100
// I believe the following three define statements are not needed for I2S only:
#define CLOCK_TYPE                  (I2S_CLOCK_44K_INTERNAL)
#define CODEC_INTERFACE_FLAGS       (WM8731_INTERFACE_FORMAT(I2S) | WM8731_INTERFACE_WORDLEN(bits16) )
#define CODEC_BITRATE               (WM8731_SAMPLING_RATE(hz44100))

/* I2S digital audio */
#include <i2s.h>

// audio data
int16_t audf, audx, audy, audd;
// timekeeping
int32_t nnn=0, i=0;

/* Since the method nextvalue() modifies the audio sample data gradually depending on the previous data, this method is used to reset the sample data to a known and constant value, which is zero for the x-channel and full scale for the y channel. */
void resetvalues()
{
  audx = 0x0000;
  audy = 0xFFFF;
}

/* This method is called once for every pair of sample data to transmit. The method provides the I2S hardware with the next sample for x and y channel by storing the sample in the audx and audy variables. */
void nextvalue()
{
  nnn++;
  if(nnn>(clock_per_sec*10))
  {
    nnn=0; resetvalues();                        // reset every 10 seconds by calling resetvalues()
  }
  if (i>1000)
  {
    audx++; audy--; i=0;                              // change values every 1000 samples by one LSB.
  }
  else
  {
    i++;       // keep counting up if i is below 1000
  }
}


/* ----------------------- DMA transfer, we get callback to fill one of the ping-pong buffers ------ */
/* Please someone explain this method to me.
pBuf seems to point to the memory that is used to store the audio data and is filled with the data provided by nextvalue(),
but what initial value does len have, how many samples are stored in one execution of this method?
This method is called by an interrupt created by the I2S-Hardware, right? */
void dma_tx_callback( int16_t *pBuf, uint16_t len )
{
  while( len>0 )
  {
    *pBuf++ = audx;
    *pBuf++ = audy;
    nextvalue();
    len--;
    len--;
  }
}


/* ----------------------- begin -------------------- */

void setup()
{
  resetvalues();
  Serial.println( "Initializing" );
  
  delay(2000); 
//  Serial.println( "Initializing." );
  delay(1000);
  I2STx0.stop();  // Stop in case it's not stopped. I2S is in defined state now.
  I2STx0.begin( CLOCK_TYPE, dma_tx_callback );  //configure clock (frequency and external/internal. Also define which function to call on interrupt  
  Serial.println( "Initialized I2S with DMA" );
  
  I2STx0.start();  // Enable I2S hardware to start working
}


/* --------------------- main loop ------------------ */
void loop()  // nothing here, because i2s is interrupt-driven once it's initialized.
{
}

This was my measurement setup:
image.jpg

I can post a screenshot of the oscilloscope screen tomorrow, don't have that in my cloud :mad:

Thanks hpyle and everyone who contributed for this awesome lib!
With best regards
Ben

Edit: The Screenshots I promised:
i2s1.png

i2s2.png
The Measurements in the lower right corner are:
Ansteigen == Rise Time
Abfall == Fall Time
Über == Overshoot (more than 20%, is this normal?)
Freq == Frequency, this is 2 channels, 16 bits each, 44.1kHz sampling rate --> 2*16*441000 = 1.4112 MHz

I hope this is useful to anyone. By the way, I have access to very good measuerment equipment. Scopes, Freq. counters, spectrum analyzers, signal generators... If this is of any use for the development of this library, let me know and I will provide you with the measurement you need.
Regards
Ben
 

Attachments

  • image.jpg
    image.jpg
    64.5 KB · Views: 335
Last edited:
Hey there, people! I'm working on an audio interface for the teensy 3, with two wolfson converters, the wm8786 and wm8740; it will also have two pga4311 digital volume controls, and an input selector to switch between low and high impedance inputs; the wm8786 is an ADC, the wm8740 is a DAC; I was wondering if both chips can share the same MCLK signal coming from the teensy, or if they MUST share it, because I don't happen to see two MCLK pins on the K20 datasheet; in fact, in page 1147, tab 46.2, of the K320 datasheet, it talks about one MCLK signal; also, I remember there was a maximum limit for the MCLK when generated by the teensy, and I can't find anymore about it in the datasheet. By the way, if you like I can post some parts of the schematic, maybe you guys could help me :)
 
I was wondering if both chips can share the same MCLK signal coming from the teensy, or if they MUST share it, because I don't happen to see two MCLK pins on the K20 datasheet;
I suppose the signal chain will be ADC->T3->DAC? In this setup the Clock needs to be either provided by the teensy or by an external oscillator. I am not shure about this, but maybe you should check if the T3's clk has a low enough jitter for your application.
Regards
Ben
 
@ben Very nice.

In your first scope screenshot: notice that the data word is delayed one bit "behind" the frame. I2S data frames start one bit later than the frame-clock, and so they also finish one bit later.

In the second screenshot, are you looking at overshoot on the bit-clock line? I wish my scope was fast enough to even see that ;)


Your code looks great. You write, /* Please someone explain this method to me... */ dma_tx_callback(...)

The dma_tx_callback, or the i2s_tx_callback if you're not using DMA, is indeed called by an interrupt. The library registers for that interrupt and implements the actual interrupt handler, which is the function i2s0_tx_isr() / i2s0_rx_isr() / dma_ch0_isr() / dma_ch1_isr in i2s.cpp. Eventually those call back to the function you passed when initializing the library.

For DMA, the library manages two buffers, each of DMA_BUFFER_SIZE samples. (That's the value of 'len' passed to you). It ping-pongs between them.
Let's start while the DMA hardware is transmitting one of the buffers. When it's done, the interrupt is raised. The library tells DMA to start transmitting from the second buffer right away; and then calls your dma_tx_callback() passing a pointer to the first buffer. So, you can spend quite a long time writing data into that buffer, and then return; when your data has been transmitted, you'll be called back again to fill the other buffer with new data.

I hope that's helpful :)
 
@MickMad I expect you can produce MCLK from teensy and clock both devices from the same signal, without using an external clock at all. But don't take my word for it; the clocking parts of the datasheet (and I2S altogether) are still very mysterious to me.
 
Heres my code for configuring Teensy 3 I2S clocks, has been tested with an WM8762

Code:
void i2s_config_clock()
{
  /* Enable I2S clock domain */
  SIM_SCGC6 |= SIM_SCGC6_I2S;
  
  /* I2S Clocks are for 48kHz */
#if (F_CPU == 96000000)
  /* MCLK = 128*Fs = 8/125 System clock at 96 MHz  */
  I2S0_MDR = (7<<12) | 124;
#elif (F_CPU == 48000000)
  /* MCLK = 128*Fs = 16/125 System clock at 48 MHz  */
  I2S0_MDR = (15<<12) | 124;
#elif (F_CPU == 24000000)
  /* MCLK = 128*Fs = 32/125 System clock at 24 MHz  */
  I2S0_MDR = (31<<12) | 124;
#endif
  /* Enable MCK out on pin 11 */
  I2S0_MCR = I2S_MCR_MOE;

  I2S0_TCR1  = 2; //  TX FIFO watermark

  /* Select MCLK and generate bitclock */
  I2S0_TCR2  = I2S_TCR2_MSEL(I2S_MSEL_MCLK) | I2S_TCR2_BCD | I2S_TCR2_BCP | 0; //  DIV(0)

  /* Enable transmit channel */
  I2S0_TCR3 = I2S_TCR3_TCE;  // TCE
  
  /* FRSZ 1 , 32 bits FS and MSB*/
  I2S0_TCR4  = (1<<16) | (31<<8) | (1<<4) | I2S_TCR4_FSD; //  MSEL(1) 
  
  /* TX Word widths and shift */
  I2S0_TCR5  = (31<<24) | (31<<16) | (15<<8); 

}

Hope it can be some help.

Regards
Magnus
 
Last edited:
@ben Very nice.

In your first scope screenshot: notice that the data word is delayed one bit "behind" the frame. I2S data frames start one bit later than the frame-clock, and so they also finish one bit later.

In the second screenshot, are you looking at overshoot on the bit-clock line? I wish my scope was fast enough to even see that ;)
Thanks. Yes, that's the bit clock. The channel clock has less overshoot, but some crosstalk from the bit clock is visible, about 500mVpp. By the way, this is the slowest scope I can work with (70MHz). I can go up to several GHz, if needed.:cool:

Your code looks great. You write, /* Please someone explain this method to me... */ dma_tx_callback(...)

The dma_tx_callback, or the i2s_tx_callback if you're not using DMA, is indeed called by an interrupt. The library registers for that interrupt and implements the actual interrupt handler, which is the function i2s0_tx_isr() / i2s0_rx_isr() / dma_ch0_isr() / dma_ch1_isr in i2s.cpp. Eventually those call back to the function you passed when initializing the library.

For DMA, the library manages two buffers, each of DMA_BUFFER_SIZE samples. (That's the value of 'len' passed to you). It ping-pongs between them.
Let's start while the DMA hardware is transmitting one of the buffers. When it's done, the interrupt is raised. The library tells DMA to start transmitting from the second buffer right away; and then calls your dma_tx_callback() passing a pointer to the first buffer. So, you can spend quite a long time writing data into that buffer, and then return; when your data has been transmitted, you'll be called back again to fill the other buffer with new data.

I hope that's helpful :)
This is very helpful, thank you so much! I added an artificial delay at the end of my nextvalue() method, and it works fine up to DelayMicroseconds(21), but the exact number of course depends on what the method does apart from delaying :D
I noticed that the speed of the whole I2S bus scales with the clock speed set in the Arduino IDE. The I2S speed is roughly correct for 96MHz (44.12 kHz) but is lower for lower clock speeds. It seems the library depends on the T3 running at the maximum frequency. Is this a bug or did I miss something?
 
Hey there, people! I'm working on an audio interface for the teensy 3, with two wolfson converters, the wm8786 and wm8740; it will also have two pga4311 digital volume controls, and an input selector to switch between low and high impedance inputs; the wm8786 is an ADC, the wm8740 is a DAC; ...By the way, if you like I can post some parts of the schematic, maybe you guys could help me :)

do you have the wm8740 running with i2s/dma? ...wondering whether it's worth the hassle to upgrade a bit from the wm8731
 
do you have the wm8740 running with i2s/dma? ...wondering whether it's worth the hassle to upgrade a bit from the wm8731

I still have nothing running, actually :D I'm finishing the circuit layout in the next weeks. By the way, the WM8740 is a state-of-the-art converter, it sure is worth the hassle :) that's why I'm doing this circuit ;)
 
I still have nothing running, actually :D I'm finishing the circuit layout in the next weeks. By the way, the WM8740 is a state-of-the-art converter, it sure is worth the hassle :) that's why I'm doing this circuit ;)

i see. what is it for?

anyways, no doubt the specs look appealing... i was just thinking aloud, whether i should bother swapping that wm8731 for something fancier on my board, that is.
 
problems with i2s_rx_dma Mikroe

Hi
I am trying to use the new libraries "hugh pyle" i2s_thru_dma_wm8731 but I'm having problems with the card "mikroE" because i only hear one headset with some noise when i speak in the microphone. Someone with the same problem?
this is the code that I am using.

Code:

Code:
/* TODO synchronous receive only if we choose it explicitly */

/*
  I2S digital audio demonstrator for Teensy 3.0
  Interfaces using Wolfson WM8731 codec.
  
  This example is a "play through delay" test,   using I2S with DMA.
  Reads input into a circular buffer, writes output from the same buffer,
  If the buffer size is DMA_BUFFER_SIZE (128 by default), this is "straight through".
*/

/*
  To use the Mikro proto board (as master): set clock type to I2S_CLOCK_EXTERNAL
  Note: this board doesn't have line-in connections.
      SCK  -> Teensy 9  (I2S0_TX_BCLK)
      MISO -> Teensy 13
      MOSI -> Teensy 3  (I2S0_TXD0).  Can also be switched to pin 22.
      ADCL -> Teensy 12
      DACL -> Teensy 4  (I2S0_TX_FS).  Can also be switched to pin 23 or 25
      SDA  -> Teensy 18 (I2C0_SDA)
      SCL  -> Teensy 19 (I2C0_SCL)[CODE]
3.3V -> Teensy 3.3v
GND -> Teensy GND
*/





//Settings for MikroE prototype board
#define CLOCK_TYPE (I2S_CLOCK_EXTERNAL)
#define CODEC_INTERFACE_FLAGS (WM8731_INTERFACE_FORMAT(I2S) | WM8731_INTERFACE_WORDLEN(bits16) | WM8731_INTERFACE_MASTER)
#define CODEC_BITRATE (WM8731_SAMPLING_RATE(hz48000))
#define CODEC_ANALOG_FLAGS (WM8731_ANALOG_DACSEL | WM8731_ANALOG_MICBOOST | WM8731_ANALOG_INSEL)


/* Wolfson audio codec controlled by I2C */
/* Library here: https://github.com/hughpyle/machinesalem-arduino-libs/tree/master/WM8731 */
#include <Wire.h>
#include <WM8731.h>


/* I2S digital audio */
#include <i2s.h>


/* Circular buffer for audio samples, interleaved left & right channel */
const uint16_t buffersize = DMA_BUFFER_SIZE; // must be a multiple of DMA_BUFFER_SIZE
volatile int16_t buffer[buffersize];
uint16_t nTX = 0;
uint16_t nRX = 0;


/* --------------------- DMA I2S Receive, we get callback to read 2 words from the FIFO ----- */

void dma_rx_callback( int16_t *pBuf, uint16_t len )
{
while( len>0 )
{
buffer[nRX++] = *pBuf++;
buffer[nRX++] = *pBuf++;
len--;
len--;
}
if( nRX>=buffersize ) nRX=0;
}

/* --------------------- DMA I2S Transmit, we get callback to put 2 words into the FIFO ----- */

void dma_tx_callback( int16_t *pBuf, uint16_t len )
{
while( len>0 )
{
*pBuf++ = buffer[nTX++];
*pBuf++ = buffer[nTX++];
len--;
len--;
}
if( nTX>=buffersize ) nTX=0;
}


/* ----------------------- begin -------------------- */

void setup()
{
Serial.println( "Initializing");
delay(5000);
Serial.println( "Initializing." );

delay(1000);
WM8731.begin( low, CODEC_BITRATE, CODEC_INTERFACE_FLAGS );
WM8731.setActive();
//WM8731.set()
// WM8731.setOutputVolume( 127 );
WM8731.set( 0x04, 0x15 );
Serial.println( "Initialized I2C Codec" );

delay(1000);
I2SRx0.begin( CLOCK_TYPE, dma_rx_callback );
Serial.println( "Initialized I2S RX with DMA" );

I2STx0.begin( CLOCK_TYPE, dma_tx_callback );
Serial.println( "Initialized I2S TX with DMA" );

// Before starting tx/rx, set the buffer pointers
nRX = 0;
nTX = 0;

I2STx0.start();
I2SRx0.start();
}



/* --------------------- main loop ------------------ */
void loop()
{
/* do nothing */
}[/CODE]

I hope you can help
 
i see. what is it for?

anyways, no doubt the specs look appealing... i was just thinking aloud, whether i should bother swapping that wm8731 for something fancier on my board, that is.

It's an usb audio interface; it will have one wm8740 dac and one wm8786 adc, two pga4311 digital volume control, two fully differential opamps to convert from unbalanced to balanced inputs (wm8786 has balanced inputs), and an input impedance selector, to switch between line input and instrument input (for bass/guitar). BTW I don't think you can swap chips that easily, as the wm8731 is a codec while these chips are dac/adc only, and they need some glue circuitry to work as stated in the datasheets. I hope that the circuit I'm working on will serve as a good starting point for a nice audio interface based on the teensy. I repeat, I will post schematics asap.
 
I did another measurement to find out the CPU load while I2S Tx is running. To do so, I held a pin high during the ISR that feeds new data to the I2S transmit buffer. I use Pin 0 for this.
It turns out the pin is high for about 4% of the time and I think this is roughly equal to the CPU load the ISR imposes. The ISR itself is relatively small, it generates a triangular pattern on one channel, changing one LSB every sample. The other channel is constantly at the lowest value.
Here is another screenshot from my scope:
scope_0.png
D0 D1 and D2 are the I2S bus. D3 is the pin I used to indicate the ISR is running (Teensy 3 Pin 0).
The measurements in the lower right corner mean:
Periode = period
Freq. = frequency (which is 1/period of course)
+Breite = signal high time (how long the ISR is active)
Arbeit = Duty cycle (what percentage of the whole time the ISR is active)

I also changed the audio data to uint16_t so it has a range starting at 0x0000. I don't know why, but this made it easier for me. Is it ok to do so and write the data to *pBuf, which is int16_t, without any conversion? It seems to work so far...

Here's my code:

/*
I2S & DMA digital audio demonstrator for Teensy 3.0
SCK -> Teensy 9 (I2S0_TX_BCLK)
MOSI -> Teensy 3 (I2S0_TXD0). Can also be switched to pin 22.
DACL -> Teensy 4 (I2S0_TX_FS). Can also be switched to pin 23 or 25
3.3V -> Teensy 3.3v
GND -> Teensy GND
*/

/* Settings for I2S Interface */
#define clock_per_sec 44100
#define CLOCK_TYPE (I2S_CLOCK_44K_INTERNAL)

#define SIGNPIN 0

/* I2S digital audio */
#include <i2s.h>


// audio data
uint16_t aud_mono;
// ramp generator rising or falling:
boolean rising;

int i=0;


/* ------------ begin ----------- */

void setup()
{
pinMode(SIGNPIN, OUTPUT);
Serial.println( "Initializing" );
delay(200);
I2STx0.stop();
I2STx0.begin( CLOCK_TYPE, dma_tx_callback );
Serial.println( "Initialized I2S with DMA" );
Serial.println( "Begin transmission in 2 seconds" );
delay(2000);
I2STx0.start();
aud_mono = 0x0000; rising = 1;
}


/* ----------- main loop ---------- */
void loop()
{
}


/* ------- DMA transfer, we get callback to fill one of the ping-pong buffers ----- */
void dma_tx_callback( int16_t *pBuf, uint16_t len )
{
digitalWrite(SIGNPIN, HIGH);
while( len > 0 )
{
*pBuf++ = aud_mono;
*pBuf++ = 0x0000;
nextvalue();
len--;
len--;
}
digitalWrite(SIGNPIN, LOW);
}

/* -------- Provide next audio sample ---------- */
void nextvalue()
{
if ( aud_mono == 0xFFFF )
{
rising = 0;
}
else if (aud_mono == 0x0000 )
{
rising = 1;
}
if (rising)
{
aud_mono++;
}
else
{
aud_mono--;
}
}

Regards
Ben
 
It's an usb audio interface; it will have one wm8740 dac and one wm8786 adc, two pga4311 digital volume control, two fully differential opamps to convert from unbalanced to balanced inputs (wm8786 has balanced inputs), and an input impedance selector, to switch between line input and instrument input (for bass/guitar). BTW I don't think you can swap chips that easily, as the wm8731 is a codec while these chips are dac/adc only, and they need some glue circuitry to work as stated in the datasheets. I hope that the circuit I'm working on will serve as a good starting point for a nice audio interface based on the teensy. I repeat, I will post schematics asap.

ah, i figured effects pedal or something. curious to see what you come up with in terms of usb audio.

and sure, i'd have to redo my board somewhat, but since i'm not using the adcs on the wm8731 i might as well ... it's running off -12/12v so the "glue circuitry" won't be a big deal.
 
I also changed the audio data to uint16_t so it has a range starting at 0x0000. I don't know why, but this made it easier for me. Is it ok to do so and write the data to *pBuf, which is int16_t, without any conversion? It seems to work so far...
int16_t is signed two's complement data - exactly what you want for i2s. If your input data was int16_t, casting to uint16_t and then back will work fine. Unsigned input data needs to be converted to signed.
 
Over the last several days, while waiting for beta testers on the low power improvement and mac bug fix, I've started working on an audio API for Teensy.

I've been fiddling with DMA for dual-PWM output (a pair of 1:256 scaled resistors for high res). Much of this work will also carry over the I2S, but for the moment I'm concentrating on PWM while I refine the API. I hope to get to I2S in a couple weeks.

Here's a scope screenshot of the DMA-based PWM output...

scope_5.png

The top 2 traces are PWM on pins 3 and 4. The red trace is a pulse from the interrupt that feeds more data into the DMA's buffer.

The blue trace is a simple R-C filtered output, using a 1K resistor on pin 3 and 240K resistor on pin 4, both driving a 10 nF capacitor. Only 4 interrupts occurred, so the buffer didn't get refreshed with new data on the far right side of the trace, where it begins replaying the stored buffer.

The math trace is a FFT. There a fair amount of DC from the initial part of the waveform, so the 1 kHz energy is pretty close to DC on this scale. In the middle, you can see energy at 43 and 45 kHz, and of course a lot of stuff around 88.2 kHz for the PWM carrier.

While this screenshot is just PWM waveforms, most of what I'm actually working on is the API and software infrastructure to make audio projects really easy. The API allows objects to be connected together, so audio data flows automatically. The idea is to free up the Arduino sketch from moving data around, so you can use Arduino style programming to control audio parameters and whatever other hardware you've connected.

For example, the code to create a continuous sine wave output:

Code:
#include "Audio.h"

AudioSineWave mysine;
AudioOutputPWM myout;

void setup() {
  while(!Serial) ;
  delay(20);
  AudioMemory(8);
  mysine.connect(myout);
  mysine.frequency(1000);
  mysine.update();
  myout.update();   // these update functions will be called automatically from a timer ISR....
}

void loop() {

}

I know this message is something of a tease. I hope to have something ready for an alpha-test release in a week or two....
 
This is great news. I understand the API will configure the DMA-Parts of the Kinetis to allow streams of audio data to flow into and out of the buffers to various peripheral devices such as the PWM-Generator, the I2S subsystem, or the DAC. Right? Do you plan to make the source of a stream selectable from an ADC, I2S input or an internal function generating the data?
The math trace is a FFT. There a fair amount of DC from the initial part of the waveform, [...]
The part of the waveform where the sine is played actually has a higher DC component than the "silence" in the first millisecond.
Maybe this question comes a bit early, but is the Sample rate equal to the PWM freq.? It'd be great if the PWM frequency could be made a multiple of the sample rate, thus reducing the required steepness (order) of the low pass filter.
I know this message is something of a tease.[...]
You bet! :D
This really is exciting news. There is so much going on in the DIY electronics community. Without people like you, Paul, we'd still be smashing rocks against each other...(well not literally maybe...) Thank You!
 
This is great news. I understand the API will configure the DMA-Parts of the Kinetis to allow streams of audio data to flow into and out of the buffers to various peripheral devices such as the PWM-Generator, the I2S subsystem, or the DAC. Right?

Yes.

Well, actually into buffers that get their pointers passed around between objects that do the sound processing operations you want. I'm designing for so much more than simply passing audio from an input to an output!

Do you plan to make the source of a stream selectable from an ADC, I2S input or an internal function generating the data?

You can create any number of audio stream objects, of course within the memory and CPU power available. Objects may be physical input or output, or purely data processing (filters, mixers, reverb, pitch shift, etc). Each object may define any number of inputs and outputs. They can be connected together in any way you like. Well, except of course in a feedback loop, which is illegal.

The API supports fanout, where an output can connect to inputs of several other objects. Internally, the audio data is only duplicated if an object needs write access to modify the data (eg, a filter). Each input can receive only a single stream (no fanin support), so if you want to connect multiple streams to an input (perhaps the input of the AudioOutputPWM object), you'd need to connect them to a mixer object, and then connect the mixer's output to AudioOutputPWM.

The API also tries to make creating new objects easier. It provides functions to receive audio blocks, either read-only or writable, to transmit blocks, and to allocate and release blocks from the memory pool. It also supports (or will soon support...) automatically calling the update function on every object automatically. That's the bit I'm doing now, and it might take me a while to really get the algorithm worked out for what order to call them all so the data flow is correct.

I've been working with a block size of 128 samples, or about 2.9 ms.


Maybe this question comes a bit early, but is the Sample rate equal to the PWM freq.? It'd be great if the PWM frequency could be made a multiple of the sample rate, thus reducing the required steepness (order) of the low pass filter.

The sample rate is 44.1 kHz (actually 48 MHz divided by 1088, which works out to be 44.117647 kHz). The PWM carrier is double the sample rate: 88.2 kHz.

I might try increasing to 176.4 kHz carrier at some point, but that's pushing the limits to very difficult timing. The DMA event occurs at the compare match, not the beginning of the cycle. It's unfortunate Freescale didn't provide a way to create trigger the DMA at the beginning of the PWM cycle (at least not any way I could find). Triggering within those last 16 clocks is dangerously close to the latency imposed by the DMA engine to load the transfer descriptor. If there's any bus arbitration, like another DMA channel, or the USB's DMA controller, or interrupt stack framing, or even a load or store multiple instruction, the DMA might get delayed until the first part of the next PWM cycle. Maybe that's not a huge problem if it's unlikely to occur, since it'll just cause one PWM pulse to be repeated instead of the correct output during that cycle.
 
Last edited:
Here's a couple more scope screenshots.

scope_6.png

I've finally got the automatic updating working, so the sine wave keeps updating continuously now. :)

The FFT in this screenshot shows only the 20 kHz audio band. I believe that harmonic distortion at 2 kHz is mostly from my poorly matched resistors. At least I hope it is.

The red trace is the CPU usage.

scope_7.png

Here you can see a zoom in to the CPU usage. The short pulses are the DMA interrupt. The long pulse is the updating interrupt, which runs at a low priority so the DMA and other interrupts can still work. During this update time, the sine wave data is being created. As more audio processing objects are created, this is where their CPU usage will go.
 
Status
Not open for further replies.
Back
Top