13_title.png

In the last section, we generated some sound on our headphones that originated an audio file on a MicroSD card.  We made use of one of Simplicity Studio’s examples to make this happen, so in this section, we will build a helper library from that example to customize the I2S driver to our application.  We will also create, fetch and play stereo audio this time around, and dive a little deeper into the role of DMA in this example.

 

As a reminder, complete code for the Maker's Guide can be found on Github.

 

Building Our Own MicroSD and I2S Library

At this point, we have some code that will play an audio file at power on, but it has to be modified to make it useful to be called when an event happens, like at a button press.  I decided to simplify my main.c file and put most of the supporting driver functions in an i2s_helper.c file.  That left me with a more succinct and easy-to-follow main.c file:

 

// main.c
// Rebuilt solution for playing .wav files over I2S
// Chapter 13
 
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
 
#include "i2s_helpers.h"
#include "utilities.h"
 
int main(void)
{
      CHIP_Init();
 
      /* Use 32MHZ HFXO as core clock frequency, need high speed for 44.1kHz stereo */
      CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);
 
      /* Start clocks */
      CMU_ClockEnable(cmuClock_DMA, true);
 
      // Get the systick running for delay() functions
      if (SysTick_Config(CMU_ClockFreqGet(cmuClock_CORE) / 1000))
      {
            DEBUG_BREAK;
      }
 
      // This creates MCLK for the I2S chip from TIMER1
      create_gpio_clock();
 
      // Enable the PB1 pushbutton on the devkit
      setup_pushbutton();
 
      // Enable the I2S output pins
      I2S_init();
 
      // Give the I2S chip time to get started
      delay(100);
 
      while (true)
      {
            int track = get_next_track();
            play_sound(track);
 
            // Debounce the switch...
            delay(750);
 
            while (!get_button())
                  ;
      }
}
 
// Define the systick for the delay function
extern uint32_t msTicks;
void SysTick_Handler(void)
{
      msTicks++;
}

As you can see, I added a button press to rotate through sounds defined in the i2s_helper.h and i2s_helper.c files:

 

 

typedef struct filename_data
{
      char filename[15];
} wav_files;
#define MAX_TRACKS 3
wav_files file_array[MAX_TRACKS] = {{"sweet4.wav"}, {"sweet5.wav"}, {"sweet6.wav"}};

That enabled me to make a simple function to get the next track:

int get_next_track()
{
      static int track = 0;
      int result = track;
      track++;
      if (track == MAX_TRACKS) track = 0;
      return result;
}

I used Audacity to convert the first two sounds to 32000 Hz to demonstrate that a different sample rate would still work.  This required a change of the MCLK to 8 MHz, and you can see that change in the i2c_helpers.c file in the create_gpio_clock function. 

 

For the third file, I created a stereo .wav file by combining the two mono sources in sweet4.wav and sweet5.wav.  You can do that by using the controls on each track to specify the first as right and the second as left, and then exporting the combination as a .wav file:


 13_audacity_channel_control.png

 

In the i2s_helper.c file, I cleaned up some things that weren’t exactly right in the wavplayer.c file.  For example, the original wavplayer.c was designed to be run on the internal DAC within the EFM32 part. We will cover the internal DAC in the next chapter, but it is limited to 12 bits and the example has it configured in single-ended mode that only accepts unsigned integers.  However, the I2S DAC is 24-bits capable and also accepts 2’s complement data.  I was able to put a compile-time switch in the code that boosted the volume when using I2S DAC:

 

#ifndef USE_I2S
      tmp = buffer[i];
 
      /* Convert from signed to unsigned */
      tmp = buffer[i] + 0x8000;
 
      /* Convert to 12 bits */
      tmp >>= 4;
 
      buffer[i] = tmp;
#endif

 

Direct Memory Access (DMA) - Ping Pong Mode

One thing that is new in this project is the use of the Ping Pong DMA mode.  Up until now, we have used single descriptor DMA tables or the DMADRV driver tool.  In this example, the author of the original wavplayer.c file needed very fast DMA transfers to keep up with 44.1 kHz audio files, which requires the use of ping pong mode.  I did not have to change a line of code in that section, but the following is a diagram of what it is doing.

 13_dma_ping_pong.png

One thing to note is that DMA is transferring data from a RAM source through to either the DAC or I2S destination, depending on the USE_I2S parameter.  In our example, we have the USE_I2S parameter set, so that means that the destination is the USART in I2S mode.  The RAM buffer is being filled from the MicroSD card over the SPI bus whenever the RAM buffer is empty.  DMA is not involved with those SPI transfers, other than to signal when it is time to fill up the empty RAM buffers.  Note that it probably could have been used there too, as long as it used a different channel than the one being used for the RAM to I2S/DAC transfers.

 

The first thing that the author does in the DMA_setup function is to initialize this DMA peripheral.  This only needs to be done once, but I have actually called it repeatedly after reset to no harm.  Here, the author chooses unprivileged DMA transfers with hprot = 0, which is an ARM core signal that means “user mode,” and it utilizes a standard control block that is included as part of the em_dma library.

 

/* DMA configuration structs */
  DMA_Init_TypeDef       dmaInit;
  DMA_CfgChannel_TypeDef chnlCfg;
  DMA_CfgDescr_TypeDef   descrCfg;
 
  /* Initializing the DMA */
  dmaInit.hprot        = 0;
  dmaInit.controlBlock = dmaControlBlock;
  DMA_Init(&dmaInit);

In the next section, the author configures the DMA channel that is specific to I2S or DAC transfers.  Whenever a channel in the DMA completes, there is a callback that is your code’s entry point into that event.  PingPongTransferComplete is the function that is defined in wavplayer.c and gets called when each DMA transfer is complete. 

 

You can see that the chnlCfg.select variable tells the DMA controller which DMA request line to use for this channel.  Since this example sends data externally onto the USART1 bus and onto the I2S CS4344 device, it uses the DMAREQ_USART1_TXBL parameter.  Had it been using the internal DAC, it would be pointing to the DMAREQ_DAC0_CH0 parameter. 

 

DMAcallBack is registered with chnlCfg.cb, and then the chnlCfg object is passed into the DMA channel config function called DMA_CfgChannel, along with the DMA channel number.

 

 

  /* Set the interrupt callback routine */
  DMAcallBack.cbFunc = PingPongTransferComplete;
 
  /* Callback doesn't need userpointer */
  DMAcallBack.userPtr = NULL;
 
  /* Setting up channel */
  chnlCfg.highPri   = false; /* Can't use with peripherals */
  chnlCfg.enableInt = true;  /* Interrupt needed when buffers are used */
 
  /* channel 0 and 1 will need data at the same time,
   * can use channel 0 as trigger */
 
#ifdef USE_I2S
  chnlCfg.select = DMAREQ_USART1_TXBL;
#else
  chnlCfg.select = DMAREQ_DAC0_CH0;
#endif
 
  chnlCfg.cb = &DMAcallBack;
  DMA_CfgChannel(0, &chnlCfg);

Now that the DMA channel is set up, DMA descriptors must be set up for the channel.  Descriptors tell the DMA peripheral where to fetch data, where to send the data, and how much data to tackle in a single transaction.  A ping pong transfer requires two descriptors that it uses alternately, hence the “ping pong” naming.  Both primary and secondary descriptors are set up on the same DMA channel and share the same interrupt callback.  This can be seen in the FillBufferFromSDcard function where it checks to see which descriptor is active so it knows which RAM buffer to fill. 

 

The following code sets up the primary and secondary descriptors exactly the same so that throughput is increased and the system can keep up with the time-sensitive demands of streaming audio.  The dstInc parameter controls how much to increment the address of the destination on each DMA transaction.  It is set to dmaDataIncNone because there is no address to increment in either USART or DAC modes.  The data is simply placed in a static register when the time is right. 

 

On the source side for the DMA transfer, the data address is incremented in the RAM buffer according to how much data can be received in a single transaction on the destination side.  Since I2S uses USART, the best we can do is 2 bytes of data at time.  If we were to fetch four bytes, we would overwrite the data in the USART before the USART was ready for more data.  If we were using a DAC for the destination, it can accept four bytes at a time, so we would fetch four bytes from the MicroSD card in each DMA transaction.

 

 

/* Setting up channel descriptor */
  /* Destination is DAC/USART register and doesn't move */
  descrCfg.dstInc = dmaDataIncNone;
 
  /* Transfer 32/16 bit each time to DAC_COMBDATA/USART_TXDOUBLE register*/
#ifdef USE_I2S
  descrCfg.srcInc = dmaDataInc2;
  descrCfg.size   = dmaDataSize2;
#else
  descrCfg.srcInc = dmaDataInc4;
  descrCfg.size   = dmaDataSize4;
#endif
 
  /* We have time to arbitrate again for each sample */
  descrCfg.arbRate = dmaArbitrate1;
  descrCfg.hprot   = 0;
 
  /* Configure both primary and secondary descriptor alike */
  DMA_CfgDescr(0, true, &descrCfg);
  DMA_CfgDescr(0, false, &descrCfg);

 

Finally, the DMA transfers are started in the code below.  This DMA_setup function is sort of mis-named.  It should be called a DMA_setup_and_start function.  I learned that when I paused the debugger after this function and still observed the transactions go out on the scope that was hooked up to the I2S bus.  I rearranged the code to first setup I2S and then call on the DMA_setup function so that I could single step through the I2S_setup function before the DMA transaction started.

 

When the DMA is activated with DMA_ActivatePingPong, the size of the entire cycle for each descriptor is given in addition to where to send the data.  When setting up the descriptor tables, we only specify how many bytes per individual DMA transfer to send at a time, but in this function we specify exactly where to send the data (USART1->TXDOUBLE or DAC0->COMBDATA) and how many bytes make up the entire DMA cycle in either 2 * BUFFERSIZE – 1 or BUFFERSIZE – 1.  

 

With the ping pong transfer type, the DMA hardware is moving a certain number of bytes per DMA cycle for one descriptor, for example the primary descriptor, while the secondary descriptor is stopped and the RAM buffers for the secondary descriptors are being refilled.  This allows a seamless streaming of audio data from MicroSD card to RAM to the USART bus and into the CS4344 I2S DAC.

 

  /* Enabling PingPong Transfer*/
  DMA_ActivatePingPong(0,
                       false,
#ifdef USE_I2S
                       (void *) & (USART1->TXDOUBLE),
                       (void *) &ramBufferDacData0Stereo,
                       (2 * BUFFERSIZE) - 1,
                       (void *) &(USART1->TXDOUBLE),
                       (void *) &ramBufferDacData1Stereo,
                       (2 * BUFFERSIZE) - 1);
#else
                       (void *) &(DAC0->COMBDATA),
                       (void *) &ramBufferDacData0Stereo,
                       BUFFERSIZE - 1,
                       (void *) &(DAC0->COMBDATA),
                       (void *) &ramBufferDacData1Stereo,
                       BUFFERSIZE - 1);
#endif
}

The DMA has a lot of steps and a lot of places to go wrong.  To make matters worse, it cannot be single-stepped or debugged very easily.  Once the DMA peripheral, channels, and descriptors are all set up and the transfer is started, the hardware will take care of the rest automatically in the background.  Any buffer overruns in that process can cause hard faults or logic errors in your program, so you have to set things up right.

 

Final Thoughts on I2S Library

The i2s_helpers.c library that I have created is not perfect.  It could use some fixes that I will leave to the reader as an exercise.  The first issue is that the CS4344 seems to power down when there are no transactions on the I2S bus for a while.  This means that sometimes when you press the button, the sound file has a stutter in the beginning.  Therefore, the FUDGE factor that is controlled by need_to_rewind variable should be controlled by a timeout.  If it has been a while since the last sound was played, the part has probably powered down, so we need the rewind.  If we just played something, then the .wav file doesn’t need to be rewound before the next play event.

 

The second issue with this library is that there is a resolution mismatch.  The CS4344 is a 24-bit DAC, and I2S is based on 32-bit transactions, but our .wav files are 16-bit samples.  The first bits that enter the DAC are the Most Significant Bits (MSB) and therefore when the DAC sees our 16-bit data, it is placing those bits at 23:16 in the voltage range.  This creates an audible pop when the sound is done playing because a voltage value of 1 in our file has a meaning of (1 << 8) or 128 voltage steps.  In order to fix this, we would need to:

  1. Change the I2S setup in the USART to 32-bit wide transactions and fix the I2S setup block up to set init.sync.format = usartI2sFormatW32D32
  2. Change the buffer sizes in RAM to int32_t, casting a 16-bit sound 2’s complement sample to 32-bits and inflating each sample with extra digits to fill the higher order bits with zeros or ones depending on positive or negative data values.

This should eliminate the pop or click at the end of each sound.

 

Note that whenever you are dealing with large arrays/buffers like the RAM buffers in this example project, you can run out of available RAM on the device quickly.  This examples reserves 2.5 KB of RAM for buffers, which isn’t too bad since the Wonder Gecko has 32 KB of RAM in total.  But Zero Gecko and Tiny Gecko parts are limited to as little as 2 KB in total, so keep that in mind.  When you use too much RAM, sometimes you will get compiler errors, but other times you will just get a fault that can be difficult to debug.

 

This wraps up the chapter on I2S sound generation.  In the next chapter, we will cover other ways to create sound and how to use the internal DAC instead of an external DAC.

 

 PREVIOUS | NEXT

  • Blog Posts
  • Makers