I2S and Reference Clocks on the PIC32MZ

What is I2S?

I2S is short for Inter-IC sound, and people call it various things like I-I-S, I-squared-S, I2S etc. but what's important is that it behaves a lot like SPI, which I covered a few posts back. To think of it very basically, it is SPI plus a few other clock signals and it's used for sending audio data to chips like Digital to Analog Converters (DACs). In today's example I will be using the CS4334 DAC, a 5V DAC that I have used for some time. I'm going to modify the existing MP3 player app to use this DAC instead of PWM.

What signals does the CS4334 need from the PIC32MZ?

Let's take a look at the CS4334's datasheet:

PIC32MZ - CS4334 Pinout

It needs SDATA, DEM/SCLK. LRCK and MCLK. Let's run over what those pins do:

  • SDATA is the actual data line, in the SPI examples before it would have been SDI/SDO.
  • DEM/SCLK I will use as SCLK, the SPI data clock, and not as DEM or de-emphasis clock.
  • MCLK is the master clock, which I will discuss below. I will set this to 256x the SCLK frequency.
  • LRCK controls whether the data being input into SDATA is for the left or right channel. It's low for left channel data and high for right channel data.

In the description for MCLK, we can see that the clock speeds needs to be 256x the sample rate of the audio. For a 44.1kHz signal, this means the MCLK clock frequency needs to be 256 x 44,100 = 11,289,600Hz.

BRM? HRM? The CS4334 has two modes and, thankfully, it auto-detects which mode we are using based on the frequency of MCLK. The modes are Base Rate Mode (BRM) and High Rate Mode (HRM). According to the datasheet, High Rate Mode allows for input frequencies up to 100kHz but we have no need for that for playing MP3s. I'm going to stick to using 44.1kHz and 48kHz MP3s and set my MCLK to 256x the MP3's frequency.

Before we look at how to set this all up, let's go over what each of those signals requires:

  • MCLK, as discussed above, needs to be 256 x 44,100Hz or 11,289,600Hz.
  • SCLK needs to send 44,100 16-bit words of data per channel each second.
  • LRCK needs to switch between high and low 2 x 44,100 = 88,200 times per second.

That SCLK calculation can be a bit tricky to understand. To ease our understanding of it, let's say that two 16-bit channels together gives us 32-bits of data for each "audio frame" and we need to send 44,100 audio frames per second.

LRCK only changes when we have sent some data, and it changes like this:

  • It starts out low, and then we send data for the left channel.
  • When the data is sent, it automatically goes high and then we send data for the right channel.
  • When the data is sent, it automatically goes low again, and this repeats until we tell it not to.

Generating all those different clock signals

As I stated before, the I2S peripheral works very similarly to how the SPI peripheral does. For the purposes of this post, one of the main differences is that the Slave Select (SS) pin can be used to automatically generate the LRCK signal for us, which is great. So that takes care of that clock signal at least, but what about that massive MCLK signal? You might think you can get away with using a timer but let's see what that'd entail:

  • If SYSCLK is 200Mhz then PBCLK3 is a max of 100MHz.
  • We need a 11.2896MHz clock signal at 44.1kHz, or even 12.288MHz at 48kHz
  • Timer period calculation gives us 100,000,000 / 11,289,600 = 8.85.

The first apparent problem is that the period needs to be a whole number, so we'll either have to round up or round down. Let's see what that gives us:

100,000,000 / 9 = 11.111MHz which we then need to divide by 256 to get our playback frequency, so 43,402Hz. Not great. Well then, how about dividing by 8?

100,000,000 / 8 = 12.5MHz / 256 = 48,828Hz. That's even worse.

Further, with a period as low as 8.85, the PIC32 is going to have to call an interrupt every 8 clock ticks and even if that's possible it's going to leave almost zero free time to run any other code. Thankfully, there's a bit of hardware that can help us and it's called the Reference Clock Output.

Using the Reference Clock Output (REFCLKO)

There are two things to know about using the Reference Clock Output as it pertains to today's post. The first is found in the datasheet under the SPIxCON register:

PIC32MZ - SPIxCON MCLKSEL selection

As that shows, we can only use Reference Clock 1 for SPI/I2S. This can also be found in the Clock Distribution section of the same datasheet:

PIC32MZ - Clock Distribution

The second is the formula for calculating the Reference Clock Output frequency. Of course, this isn't in the main datasheet but in the separate DS60001250. This shows us the formula:

PIC32MZ - Reference Clock Frequency Formula

That's perhaps not as easy to understand. Let's see how it works. We have two variables:

  • RODIV - Reference Clock Divider, which is a 15-bit number so can have a value from 0 to 32,767 and
  • ROTRIM - Reference Clock Trim, which is a 9-bit number that can have a value from 0 to 511.

Whereas normal clocks just have a Divider, the Reference Clocks have a Trim variable to help provide fractional clock speeds. This means we can get much closer to our desired frequency than we can with timers. FREFIN refers to the frequency of the clock source we are using for the Reference Clock. This is defined by the ROSEL bits in REFOxCON:

PIC32MZ - ROSEL bits

Phew. Lots of pictures today. Basically, when we're using the Reference Clock I'm going to be use PBCLK1 as the source and in all my example PBCLK1 is running at 100MHz.

To understand how setting the frequency works, let's look at how it works. For this example, we want an output frequency of 256 * 44,100Hz, or 11,289,600Hz:

  • FREFOUT = FREFIN / [2 * (RODIV + ROTRIM/512)] can be rearranged to give us RODIV + ROTRIM/512 = FREFIN / (2 * FREFOUT).
  • For us, that gives RODIV + ROTRIM/512 = 100,000,000 / (2 * 11,289,600), which is 4.4289.
  • I will set RODIV to 4 and ROTRIM to (0.4289 * 512), which is 219.5968, so I'll round that up to 220.

Let's plug 4 and 220 back into the formula and see what we get out:

FREFOUT = FREFIN / [2 * (RODIV + ROTRIM/512)] so FREFOUT = 100,000,000 / [2 * (4 + 220/512)] = 100,000,000 / 8.859375 = 11,287,477Hz. Let's divide that to see what sample rate this translates to for our MP3 playback:

11,287,477 / 256 = 44,091Hz. That's only 9Hz off what we want, that's great. The Reference Clock Outputs all have a bit of error called jitter so they're not perfect but they're more than good enough for what we want to do.

BEWARE Something that tricked me when I first started working with the Reference Clock was that my frequency was frequently (har har) wrong. It turned out that this was the culprit:

PIC32MZ - REFOxTRIM warning

ONLY THE UPPER 9 BITS OF REFOxTRIM ARE USED. This means that when we set the Trim value, we must shift the value left by 23, like such:

REFO1TRIM = Trim << 23;

There is no separate REFOxDIV register, so this problem is avoided by using the named bits, like such:

REFO1CONbits.RODIV = div;

OK, back to regularly scheduled programming.

There's only one clock signal that we have yet to set up, and that's the SPI Clock.

Setting up the SPI Clock (SPICLK) to use the reference clock

If you'll recall, we're wanting to send stereo 44,100Hz 16-bit data to the DAC via I2S. Our Reference Clock Output (REFCLKO) is now outputting a signal that is 256 * 44,100. How fast does our signal need to be?

We want to send 16 bits (one 16-bit sample per left and right channel) 44,100 * 2 times a second because of stereo. So we need to be running at 88,200 * 16 = 1,411,200Hz. Thankfully, we don't have to set up yet another Reference Clock Output we can just use the SPI peripheral's built-in divider called the SPI Baud Rate Generator, or SPIBRG.

Our REFCLKO output is 256 x the sample frequency of 44,100 and we want our SPI signal to be (16+16) * 2, or 64 times 44,100. So let's do some easy maths for once: 256 / 64 = 4. The SPIBRG formula was:

PIC32MZ - SPI BRG formula

As we're dividing by 4, we want the bottom half of that fraction to be 4 so we will need to set SPIBRG to 1.

Setting up SPI audio mode

In my example, the pins will be connected as follows:


PIC32MZ - I2S DAC Connections


Note: The communication with this DAC is one way, we just send it data and it processes it automatically. So we don't need to use SDI3, we can disable it.
Let's take a look at how to initialise I2S on the PIC32MZ. It's actually laid out in the datasheet DS60001106 for us like this:

PIC32MZ - Enable Audio Mode



Let's see how that looks in C:

void I2S_init(int frequency)
{    
    unsigned long int ref_freq; // The desired output frequency
    int div, trim;              // RODIV and ROTRIM
    float calc;             // Fractional values
    unsigned short dummy; // Used only to clear the data from SPI3BUF

    /* 1. If using interrupts, disable the SPI interrupts in the respective IECx register. */  
    IEC4bits.SPI3TXIE = 0;

    /* 2. Stop and reset the SPI module by clearing the ON bit (SPIxCON<15>). */  
    SPI3CONbits.ON = 0;
    /* 3. Reset the SPI audio configuration register, SPIxCON2. */  
    SPI3CON2 = 0;
    /* 4. Reset the baud rate register, SPIxBRG. */  
    SPI3BRG = 0;

    /* 5. Clear the receive buffer. */  
    dummy = SPI3BUF;

    /* 6. Clear the ENHBUF bit (SPIxCON<16>) if using Standard Buffer mode or set the bit if using Enhanced Buffer mode */  
    SPI3CONbits.ENHBUF = 1;

    /* 7. If using interrupts, perform these additional steps:
        a) Clear the SPIx interrupt flags/events in the respective IFSx register. */  
    IFS4bits.SPI3TXIF = 0;

    /*  b) Write the SPIx interrupt priority and subpriority bits in the respective IPCx register. */  
    IPC39bits.SPI3TXIP = 3;
    IPC39bits.SPI3TXIS = 1;    

    /*  c) Set the SPIx interrupt enable bits in the respective IECx register. */  
    IEC4bits.SPI3TXIE = 1;

    /* 8. Clear the SPIROV bit (SPIxSTAT<6>). */  
    SPI3STATbits.SPIROV = 0;

    /* 9. Write the desired settings to the SPIxCON2 register. The AUDMOD<1:0> bits (SPIxCON2<1:0>) must be set to ?00? for I2S mode and the AUDEN bit (SPIxCON2<7>) must be set to ?1? to enable the audio protocol. */  
    SPI3CON2bits.AUDEN = 1;     // Enable Audio mode
    SPI3CON2bits.AUDMONO = 0;   // Enable Stereo mode
    SPI3CON2bits.AUDMOD = 0;    // Type of audio mode is I2S
    SPI3CON2bits.IGNROV = 1;    // Ignore receive overflows
    SPI3CON2bits.IGNTUR = 1;    // Ignore transfer underruns

    /* 10. Set the SPIxBRG baud rate register */
    SPI3BRG = 1;                // Yields 64x the sample rate, for 16-bit stereo playback

    /* 11. Write the desired settings to the SPIxCON register:
        a) MSTEN (SPIxCON<5>) = 1. */  
    SPI3CONbits.MSTEN = 1;
    /* b) CKP (SPIxCON<6>) = 1. */  
    SPI3CONbits.CKP = 1;
    /* c) MODE<32,16> (SPIxCON<11:10>) = 0 for 16-bit audio channel data. */  
    SPI3CONbits.MODE16 = 0;
    SPI3CONbits.MODE32 = 0;

    /* Additional settings needed for using the reference clock */  
    SPI3CONbits.DISSDI = 1;     // Disable SDI pin  
    SPI3CONbits.FRMPOL = 0;     // Frame pulse is active-low  
    SPI3CONbits.CKE = 0;        // Serial output data changes on transition from idle clock state to active clock state
    SPI3CONbits.SMP = 1;        // Input data sampled at end of data output time
    SPI3CONbits.MCLKSEL = 1;    // PBCLK is used by the Baud Rate Generator

    /* Before turning on SPI3, let's set up the Reference Clock 1 (REFO1CLK) */  

    // Set up REFO1CLK to be 256 x MIXER_FREQ and then times 2 again due to the formula
    calc = frequency * 256 * 2;

    // Calculate the values for RODIV and ROTRIM
    calc = (SYS_FREQ / 2 / calc); // At 44100Hz, this gives us 4.4288

    // Store the integer part in div (at 44100Hz, 4)
    div = (int)calc;
    // Subtract the integer part (at 44100Hz, 0.4288)
    calc -= div;
    // Multiply it by 512 to get it as a fraction of 512
    calc *=  512;
    // Store the integer part in trim
    trim = (int)calc;

    REFO1CON = 0;               // Reset the Reference Clock 1 configuration
    REFO1CONbits.RODIV = div;   // Set divider
    REFO1CONbits.ROSEL = 1;     // Source is PBCLK1
    REFO1TRIM = trim << 23;     // Shift the bits 23 places to the left

    // Enable the output of Reference Clock 1
    REFO1CONbits.OE = 1;
    // Turn on Reference Clock 1
    REFO1CONbits.ON = 1;

    // Turn on the SPI3 peripheral
    SPI3CONbits.ON = 1;
}

Boy, that is a lot of code. Something to be copy and pasted if ever such a thing existed :)
OK, now we've got SPI3 set up in I2S mode and it's being clocked by Reference Clock 1. Next, we need to write the interrupt handler:

void __attribute__((vector(_SPI3_TX_VECTOR), interrupt(ipl3soft), nomips16)) SPI3TX_handler()
{    
    // Clear the SPI3 Transfer Interrupt Flag
    IFS4bits.SPI3TXIF = 0;

    SPI3BUF = playback_buffer[pb_readpos];

    // Increment the read position in the playback buffer
    pb_readpos++;

    // Check if the read position has exceeded the length of the playback buffer. If so, restart it at 0.
     if (pb_readpos >= PLAYBACK_BUFFER_SIZE) 
         pb_readpos = 0;
}

That's very similar to the interrupt handler from last time so there's nothing much to discuss.

Final setup

The final thing to do is set all the PPS settings needed by this program:

// PPS for I2S outputs
RPD15R = 0b1111; // D15 = REFCLKO1
RPB10R = 0b0111; // B10 = SDO3
RPB15R = 0b0111; // B15 = SS3

Another difference is that we don't need to convert the 16-bit audio to 11-bit anymore, so we can just do this directly:

playback_buffer[pb_writepos] = MP3_OUTPUT_BUFFER[count];

The post before this one shows the benefits of using a proper, even if fairly cheap, DAC versus using PWM. Good luck and as always post any questions in the comments.

Here's the code

Tags: code, I2S, audio, MP3, interrupts

The difference between PWM and an I2S DAC

Pulse Width Modulation (PWM) vs an Inter-IC Sound Digital to Analog Converter (I2S DAC)

I spent a long time today capturing images from my scope in preparation for a post about using I2S to output audio. I thought it'd be interesting to see the differences between audio playback using PWM with no RC filter, the two-order RC filter from yesterday and then a cheap I2S DAC (the CS4334). The results were interesting and I'll post something about them in my next post on getting I2S to work. Until then, enjoy this 80's-style monstrosity I whipped up in Photoshop!

PIC32MZ - PWM vs I2S DAC

Tags: code, PWM, I2S, audio

Playing MP3s using PWM audio

Credits and disclaimers:

  • Naturally, I didn't write the Helix MP3 decoder. It can be found on the Helix website.
  • I made extensive use of the information provided by Microchip.
  • The Helix MP3 code is based heavily on code I found by user derkling on GitHub.
  • The song is Beat your competition by Vibe tracks, found in the YouTube Audio Library.

If any of the above turn out to be copyrighted, or the owner doesn't want them online anymore, please contact me and I'll change/remove them as necessary.

Update 2018-11-21: For some reason it doesn't like the way I'm converting 16-bit signed numbers to 11-bit unsigned. For now I've modified the type of playback_buffer to int because that fixes it. I've adjusted the playback buffer size to half of what it was to compensate for the extra RAM usage. It doesn't cause any exceptions so I'm not sure what's wrong. If anyone has any ideas please do let me know :)

Playing MP3s on the PIC32MZ

Ever since I started writing this site I've been wanting to write this post. In fact, almost every post leading up to this point had this in mind. For years I wanted to be able to play MP3s from my microcontroller and have seen countless solutions for Arduino and PIC but most of them use an external decoder like the VS1053b. I've also been aware of the PIC32 Helix MP3 decoder for some time but I never wanted to sit down and look at the source code because ain't nobody got time for that.

A few months ago I eventually had enough willpower to sit down and make everything work. To my surprise, I was able to get MP3s working after a few (stressful) days of tinkering. At this point, my modifications can even run on a PIC32MX170F or PIC32MX270F chip and they only run at 40MHz! The main requirements for the MP3 player are as follows:

  • 28000 bytes of RAM (heap) for the decoder
  • 1940 bytes of RAM for the encoded MP3 data buffer
  • 4608 bytes of RAM for the decoded MP3 data
  • 4608 bytes of RAM for the playback buffer

This totals 39,156 bytes of RAM needed, no problem for the PIC32MZ. Some of those sizes can be tweaked but those are the numbers I am using in my implementation. It should be noted that compiler optimisation is necessary. You need to change to O1 at least, possibly even O3 depending on your device. I found that the PIC32MX series needed O3 to work.

<TL;DR>
Compiler optimisation? If you go to the Production menu -> Set Project Configuration -> xc32-gcc -> Option categories -> Customise you can see five options for Optimisation level:

  • 0 - This is the default. It's also the only option where debugging works nicely. However, it generates horribly inefficient code that almost seems intentionally bad.
  • 1 - This is the level you should use if you have any timing specific code.
  • 2 - Levels 0 and 1 are free but levels 2 and up require a license. Level 2 is better than level 1 but I don't use it for my little projects.
  • 3 - The best optimisation level. This is required to use the MP3 decoder on the 40Mhz PIC32MX parts.
  • s - This level optimises for smaller code while also trying to be as efficient as it can.

For my purposes, levels 0 and 1 are fine. I'm not writing code for devices I'm just making projects in my spare time :)

OK so then what is heap size? Heap is a term that means memory is allocated dynamically (so while the program is running) and not in the code (such as an array). The MP3 decoder uses malloc() to allocate memory for itself and thus cannot work without a heap. This has to be manually set up in your project. Go to Production menu -> Set Project Configuration -> xc32-ld -> Heap size (bytes) and type in 28000. That should be enough but if you run into random errors with some songs maybe try increasing it.
</TL;DR>

When you open the attached ZIP file, you will see two folders:
* PIC32MZ_11.X - The project itself, with all of its many files.
* SD card - This folder contains beat.mp3, which must be copied to the root directory of your SD card in order for this program to work.

The flow of the program

PIC32MZ - MP3 player flow chart

Let's take a brief look at those individually.

Configuration and initialisation of peripherals

There is one new thing here, related to SD card setup:

// Enabling internal pull-up bits to counter potential problems
CNPUBbits.CNPUB3 = 1;
CNPUBbits.CNPUB5 = 1;

I've enabled the internal, weak pull-ups on SDI2 and SDO2 to counter any potential problems others may have. I usually put 10k pull-up resistors on my physical dev boards to counter these issues and that's why I didn't get why my code was working last time. Hope this helps!

Apart from that, there's really nothing out of the ordinary here. Please bear in mind that my code uses the following settings:

  • SD card is set up to use SPI2, with CS on RA0, MISO (SDI2) on RB3, MOSI (SDO2) on RB5 and Clock (SCLK2) on RG6 (SCLK2 is hard-wired and cannot be changed).
  • PWM outputs are set up as follows: OC2 is connected to RC2 and OC6 is connected to RC1.

If you need to adapt this code, please bear all of those in mind. SD card changes can be made in mmcpic32.c as discussed before, but changes to the PWM outputs need to be made both in the PPS setup section in main(), that is:

// PPS for PWM outputs
RPC1R = 0b1100;         // RC1 = OC6
RPC2R = 0b1011;         // RC2 = OC2

As well as in the PWM_init() function:

OC2CON = 0;             // Turn off Output Compare module 6
OC2CONbits.OCTSEL = 1;  // Interrupt source for this module is Timer 3
OC2CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
OC2RS = 0;              // Keep the signal low for the entire duration

OC6CON = 0;             // Turn off Output Compare module 6
OC6CONbits.OCTSEL = 1;  // Interrupt source for this module is Timer 3
OC6CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
OC6RS = 0;              // Keep the signal low for the entire duration
OC2CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)
OC6CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)    

Opening the file and finding the so-called MP3 Sync Word

Nothing out of the ordinary here either. The program attempts to open a file named "beat.mp3" in the root directory of the SD card. On success, it'll attempt to intialise the MP3 decoder like this:

// Initialise the MP3 decoder
mp3Decoder = MP3InitDecoder();

if(mp3Decoder == 0)
{
}

If the MP3 decoder could not be initialised, mp3Decoder will be equal to 0 (null) and the program will not work. If this happens, the first thing you should attempt is to increase the heap size as I mentioned earlier in the post.

From there, chunks are read from the file until the MP3 Sync Word is found. This is a special marker that marks the start of a valid frame. After that, it attempts to read the data from the frame. If the frame is valid, the first phase of MP3 reading is complete and we now have the song's frequency, number of channels and bit depth.

PLEASE NOTE: This example program will only work with 16-bit stereo MP3 files. It should be fairly easy to modify it to work with mono or other bit depths.

Streaming the remaining frames from SD card and playing them back via two PWM channels

From here, the program simply reads data from the MP3 file on the SD card into a buffer and attempts to decode the MP3 frames as they are read. It outputs the data to a playback buffer that the Timer 3 interrupt will use to output the audio data.

My program uses something called a "circular buffer" for the playback data. All this is, is an array that works as follows:

  • The interrupt routine keeps track of the position it is reading from in pb_readpos.
  • The main streaming loop keeps track of where it is writing in pb_writepos.
  • Writing to the array is very fast on the PIC32MZ, much faster than the comparatively slow Timer 3 interrupt that operates at 1/44100s or every 22 microseconds.
  • Writing to array continues until the reading position = the writing position, at which point the writing waits until the reading position has moved on and then writes some more.
  • Once the writing part runs out of data to write, the program decodes another MP3 frame and starts writing again. This continues until the entire MP3 file has been played.

I prefer this system to other ones like double-buffering because there's only one array. It also writes data as soon as it can, pretty much ensuring there will be no stutters or hitches in the playback. You can easily do other things in between as the playback buffer is fairly big and can easily be made even bigger. The size is defined at the top of the code:

#define PLAYBACK_BUFFER_SIZE 2304 * 4

This provides 9,216 words of data which is four times the MP3 decoder's output buffer size. At 22 microseconds per word, is about 203 milliseconds. If your program is experiencing underrun errors and the audio is stuttering, increase this value. If you need more RAM, decrease it.

Clean-up

Once all the frames are played (or an error occurs), the very first thing the program does it call PWM_stop(). If you don't do this, the program will keep playing the last 203 milliseconds of data over and over again and nobody wants that.
After that, it closes the MP3 file handle with f_close() and frees the memory used by the MP3 decoder.

That's it for general code overview, let's take a look at some more in-depth things. This entire next section can be considered <TL;DR> :)

Things to be aware of when playing MP3s back via PWM

Converting signed 16-bit numbers to unsigned 11-bit

Most MP3s I've seen are encoded at 44.1kHz or 48kHz signed 16-bit stereo. Let's think about what that means in terms of playback.

The frequency is not a problem, we already did 44.1kHz last time. The real problem is the 16-bit signed audio. Let's take a look at our timer calculation again:

PR3 = SYS_FREQ / 2 / frequency;  

Plugging in some numbers for 44.1kHz, we can see that we get:

PR3 = 200,000,000 / 2 / 44,100 = 2,267

OK, but so what? Well, 16-bit numbers can hold up to 2^16 - 1, or 65,535. Further complicating matters is that this audio is signed, meaning is uses the first bit as a +/- sign and the rest of the 15 bits as data.
However, the most resolution we can get at 44.1kHz is 11-bit (as 2^11 - 1 = 2,047). Thankfully, this still sounds pretty good. So our first order of business will be to convert that 16-bit sign data into the 11-bit unsigned data our PWM channels expect. We can do it as follows:

output_data = ((input_data + 32,768) >> 5)

<TL;DR>
Signed 16-bit audio data is represented as a wave with audio centered around 0. Unsigned 16-bit audio data is represented as a wave centered around 32,768. So effectively, all we need to do is shift the entire waveform up by 32,768 positions to shift the center-point. That's why we add 32,768.

Because this is the tl;dr section, let's refresh how signed binary numbers work again. Let's take 3,000 and -3,000 as examples and see how they look in binary.
3,000 = 0000 1011 1011 1000b -3,000 = 1111 0100 0100 1000b

Hmmmm... what? Negative binary numbers are generally represented using something called Two's complement, which is itself a modification of One's complement. It works as follows:

  • Write down the number in binary
  • Toggle all the bits, changing 0's to 1's and 1's to 0's
  • Add 1 to the answer

So for our example, we have 3,000:

  • 3,000 = 0000 1011 1011 1000b
  • Toggle : 1111 0100 0100 0111b
  • Add 1 : 1111 0100 0100 1000b

Which was our answer to start with. For more information, Google Two's complement, it really isn't the focus of today's post.

Finally, we shift right by 6 bits because we want to discard the lower 6 bits and only keep the more important upper 10 bits.
</TL;DR>

Stereo audio from PWM

Thankfully, this is an easy one. You just need to set up two Output Compare modules. In my example, I've used OC2 and OC6. If you remember the first post on PWM, there is one thing you need to be aware of:

PIC32MZ - Output Compare Timer Select

If, for whatever reason, you have set the OCACLK bit in CFGCON to 1, the Output Compare modules use different timers for their operation. As I don't do that, I don't have such an issue.

OK, good luck with that. If anybody reads this and tries it, I'll be happy to help in the comments. I realise the number of PIC32MZ hobbyists like me out there probably isn't very high but I hope it helps.

Here's the code

Tags: code, PWM, audio, MP3

Using Pulse Width Modulation (PWM) to play audio

Using Pulse Width Modulation (PWM) to play audio on the PIC32MZ

Credit: I'm using an audio sample of a dog barking twice from a site called freesoundeffects for today's program. I do not know the site, I found it for the first time while looking for a free sound effect today. Thanks to the contributors of this sound effect, Partners In Rhyme.

PWM audio?

That's right, the varying pulses that we send out via PWM can actually be used to generate analog audio signals. The fidelity isn't super high but it's actually good enough to listen to music with. You may have heard of Digital to Analog Converters (DACs), chips that take digital data in (which can be thought of as square waves) and spit out analog sine waves. Let's take a look at the image from the last post again:

PIC32MZ - 20ms PWM signal

That's a 1ms pulse in a wave with a period of 20ms, and it was used to control a stepper motor. It's a nice square wave that goes high (logic 1, or 3.3V here) for 1ms and low (logic 0, or 0V here) for 19ms but what does that actually mean? If you were to hook up a multimeter to that output, what voltage would you expect to see? Let's calculate:

  • We know the duty cycle (high time divided by low time) is 1 / 20, which gives us 0.05, or a 5% duty cycle.
  • Our maximum value is 3.3V, so we need to see what 5% of that is. 3.3 * 0.05 = 0.165V

So if we attached a multimeter, we'd expect to measure 0.165V and in fact we do get that, or something very close. If the duty cycle were 50% we'd get 1.65V out, if it were 100% we'd get 3.3V out, etc. OK, so how does that help me make audio signals?

Well, let's look at a sine wave:

PIC32MZ - A professionally simulated sine wave

Yes, my drawing skills are amazing, I know. You can see that the sine wave moves up and down between 0V and 3.3V and has a mid-point of 1.65V. By using PWM, we can output these voltage levels too. The end result won't be as smooth as a sine wave, it'll be a square wave with a rapdily changing duty cycle, but to our ears it'll hopefully at least sound similar.

Outputting an audio buffer via PWM

Today, we're going to be looking at outputting a 44.1khz 8bit mono audio buffer via a PWM channel.

<TL;DR>
For converting audio clips to 44.1khz 8bit mono, I use Audacity and I Export Audio as "Other uncompressed file", RAW and unsigned 8bit PCM. I then use the bin2h utility to convert the audio file into a C-style array I can include in my program.
Be warned: bin2h excellently converts the file to an array but make sure to go into the header file and change the array type from char to unsigned char or your ears will hate you.
</TL;DR>

First up, let's set up a timer for 44.1khz. I'm going to use timer 3 today.

void PWM_init(int frequency)
{
    T3CON   = 0x0;      // Disable timer 3 when setting it up
    TMR3    = 0;        // Set timer 3 counter to 0
    IEC0bits.T3IE = 0;  // Disable Timer 3 Interrupt

    // Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by the frequency we want.
    PR3 = SYS_FREQ / 2 / (frequency);

    // Set up the pre-scaler
    T3CONbits.TCKPS = 0; // No pre-scale

    IFS0bits.T3IF = 0;  // Clear interrupt flag for timer 3
    IPC3bits.T3IP = 5;  // Interrupt priority 3
    IPC3bits.T3IS = 1;  // Sub-priority 1
    IEC0bits.T3IE = 1;  // Enable Timer 3 Interrupt

    OC6CON = 0;             // Turn off Output Compare module 6
    OC6CONbits.OCTSEL = 1;  // Interrupt source for this module is Timery (Which is Timer 3 for OC6)
    OC6CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
    OC6RS = 0;              // Initially keep the signal low for the entire duration
    OC6CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)    

    // Turn on timer 3
    T3CONbits.TON   = 1;
}

<TL;DR>
Firstly, you'll notice that I'm using interrupts despite stating last time that one of the advantages of PWM was that I didn't need interrupts. That is true, but let's consider what outputting 8bit audio at 44.1khz means:

  • Every 22.67 microseconds (1/44100s) I need to output a byte of data
  • This byte of data has a value ranging from 0 to 255, representing how long the pulse needs to remain high and after that time the pulse needs to go low

If you were to do this via a loop, you could also do this:

while (buffer_pos < buffer_length)
{
    OC6RS = buffer[buffer_pos];
    buffer_pos++;
    delay_us(23);
}

The big problem is that while we are doing this we can do nothing else in our code. It's much better to stick this in an interrupt and let the PIC32MZ process the data in the background while we do other things, like stream data from a file.

Secondly, I'm not using a pre-scaler. Why not? PBCLK3 is 100MHz, so PBCLK3 / frequency is 100,000,000 / 44100 which gives us 2268 and change. This means I don't need to use a pre-scaler in today's example.
</TL;DR>

Next, let's take a look at what I do in the Timer 3 interrupt handler:

void __attribute__((vector(_TIMER_3_VECTOR), interrupt(ipl5soft), nomips16)) timer3_handler()
{
    IFS0bits.T3IF = 0;  // Clear interrupt flag for timer 3

    OC6RS = buffer[buffer_pos]; // Output next byte to OC6
    buffer_pos++;               // Increase buffer position

    // Check that we're still in the range we want
    if (buffer_pos >= buffer_length) 
        buffer_pos = 0;         // If not, restart at the beginning
}

Every time the timer interrupt calls, 1/44100 seconds will have passed and it's time to output the next byte. So, I write it to OC6RS and increment the position. After that, I check that we haven't gone past the end of the buffer and if we have I restart the sample by setting the position back to 0.

<TL;DR>
For the sake of simplicity, today's example just uses 1/44100 as a value for PR3 and outputs the 8bit value to OC6 directly. However, you may notice there's something wrong here. 8bit values range from 0 to 255 and the PR3 value is 2268. This means that for the vast majority of my output signal my output will be 0, because even the maximum value of 255 only yields a duty cycle of (255/2268) which is 11.2%. All this will change, though, is the volume at which the playback occurs because it changes the voltage output level (quite drastically). If you're seeking loudness above all else, multiply the value written to OC6RS by 2, 4 or even 8 if you so choose.
</TL;DR>

Inspection of actual audio output

Remember when I said the PWM was just outputting pulses of varying duty cycle? Let's take a look on the oscilloscope what is actually being put out by OC6:

PIC32MZ - PWM audio with no filter

OK, at least the frequency is what I expect it to be but that shape is pretty awful compared to a sine wave. So what I'm going to do is add a very simple low pass RC filter at the output of OC6. A low pass RC filter looks like this:

PIC32MZ - An RC low pass filter

The values 75 ohms and 0.1uF come from the low pass filter formula, which is ƒcutoff = 1/(2πRC). I want the frequency to be cutoff from 22kHz, which is called the Nyquist frequency of a 44.1kHz signal. So I chose a capacitor value of 0.1uF and moved the equation around to get a resistor value of 75 ohms.
When I used the scope again to look at the point labelled OUTPUT I saw this:

PIC32MZ - PWM audio with one RC filter

"OK, that' better!" I thought and I felt very clever indeed. I remembered something about adding a "second stage" to my low pass filter and adjusted some values around like this:

PIC32MZ - A two-stage RC low pass filter

This time on the scope, I saw this:

PIC32MZ - PWM audio with two RC filters

Which, to my credit, is a lot more like a sine wave than I initially started with. However, there's a rather large problem. Or small, depending on how you look at it. These low pass filters are wonderful and I'd argue even necessary for PWM but they come with a cost. If you look at the peak-to-peak voltage levels in those three pictures you'll see:

  • No filters gives me 2.830V
  • Adding the first low pass filter gives me 1.850V
  • Adding another stage gives me 1.160V

Yes, the average voltage isn't dropping as wildly because the waves are smoother, but the overall average voltage is still dropping considerably. This voltage drop causes the volume of the output signal to drop noticably. There are lots of other filters you can look at (perhaps an RLC filter) but that's far beyond the scope of what I want to discuss today.

IN SUMMATION: Adding a simple RC low pass filter can smooth the output and remove any unwanted high frequency whine caused by the PWM signal. Further stages do make the wave shape better but reduce average voltage level too much.

Here's the code

Tags: code, PWM, audio