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

Pulse Width Modulation (PWM) and servos on the PIC32MZ

First, an update on why my timing has been off by a factor of two until now (doh!). It all comes down to this line:

   PB7DIVbits.PBDIV = 1; // Peripheral Bus 7 Clock Divisor Control (PBCLK7 is SYSCLK divided by 1)

It was supposed to be this:

   PB7DIVbits.PBDIV = 0; // Peripheral Bus 7 Clock Divisor Control (PBCLK7 is SYSCLK divided by 1)

I've gone and updated all the old source code, sorry for the dumb mixup :)

Additionally, I've finally started moving my source code to Github. If my site ever goes down, you can find the code here.

Using Pulse Width Modulation (PWM) on the PIC32MZ

Pulse Width Modulation (PWM) overview

Please note: In this post I'm just going to be talking about using PWM in order to send pulses of varying period, not in the context of creating a variable voltage output. So yes, there's a lot more to it than I'm going to cover today. On with the post!

Using the timer code from the last post (on timers), we can create an interrupt handler to give us a pulse like this:

PIC32MZ - 1kHz timer scope verification

That all happens in the background thanks to interrupts. However, the PIC32 series comes with a much better way to generate pulses, and it's all handled by the PIC32 in hardware. The module is called the Output Compare (OC) module, and it works as follows:

  • It's attached to a specific timer
  • When a timer ticks occurs, the value of the timer's count register (TMRxCNT) is compared to the value in the Output Compare Register (OCRx), and if TMRxCNT is bigger, one of several user-defined things can happen.

For today, we're looking at the PWM mode of the Output Compare module. What this does, is when the value TMRxCNT is lower than the value in OCRx, it sets the output OCx high. When TMRxCNT eventually gets bigger than OCRx, it sets the output OCx low. So how does this help us? Let's take a look at an example:

Say I want to generate a pulse that has a frequency of 1Hz (i.e. once every second) and I want to set port pin RC1 high for the first half and low for the second half of the pulse. Using an interrupt handler, I could do the following:

volatile int counter;

void init_timer2()
{
    T2CON   = 0x0;      // Disable timer 2 when setting it up
    TMR2    = 0;        // Set timer 2 counter to 0
    IEC0bits.T2IE = 1;  // Disable Timer 2 Interrupt

    // Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by 1000Hz and then divided by 8 for our chosen pre-scaler.
    PR2 = SYS_FREQ / 2 / 1000 / 8;

    // Set up the pre-scaler
    T2CONbits.TCKPS = 0b011; // Pre-scale of 8

    IFS0bits.T2IF = 0;  // Clear interrupt flag for timer 2
    IPC2bits.T2IP = 3;  // Interrupt priority 3
    IPC2bits.T2IS = 1;  // Sub-priority 1
    IEC0bits.T2IE = 1;  // Enable Timer 2 Interrupt

    // Turn on timer 2
    T2CONbits.TON   = 1;
    counter = 0;
}

void __attribute__((vector(_TIMER_2_VECTOR), interrupt(ipl3soft), nomips16)) timer2_handler()
{
    IFS0bits.T2IF = 0;  // Clear interrupt flag for timer 2
    counter++;
    if (counter < 500)
        LATCSET = 1;
    else
        LATCSET = 0;

    if (counter >= 1000) counter = 0;
}

And yes, that would work fine. However, it is unnecessarily complicated. With PWM it's much less code intensive because it's all handled in hardware. Let's take a look at that same code but using the Output Compare module in PWM mode:

void init_timer2()
{
    T2CON   = 0x0;      // Disable timer 2 when setting it up
    TMR2    = 0;        // Set timer 2 counter to 0

    /* Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by 1000Hz and then divided by 8 for our chosen pre-scaler. */
    PR2 = SYS_FREQ / 2 / 1000 / 8;

    // Set up the pre-scaler
    T2CONbits.TCKPS = 0b011; // Pre-scale of 8

    // Turn on timer 2
    T2CONbits.TON   = 1;

    OC6CON = 0;             // Turn off Output Compare module 6
    OC6CONbits.OCTSEL = 0;  // Interrupt source for this module is Timer 2
    OC6CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode
    OC6RS = SYS_FREQ / 2 / 1000 / 8 / 2;    // We want half the length of PR2
    OC6CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)
}

And that's it. Notice what's missing?We don't need to make an interrupt handler that wastes processing time and we don't even need to set up the interrupt for Timer 2. The PIC32's Output Compare module will handle it all for us! Let's take a look at the code:

   OC6CONbits.OCTSEL = 0;   // Interrupt source for this module is Timer 2

Output Compare Timer Select (OCTSEL) selects which timer to use as the source of interrupts. It can have either a 0 or 1 value, which will set it to Timerx or Timery. What does that mean? Let's look in the datasheet:

PIC32MZ - Output Compare Timer Select

OK, as you can see, certain OC modules work with certain timers. Do not just assume they all work with Timer 2 or 3 :) In my example, OC6 works with Timer 2 (x) or Timer 3 (y) and I've set up Timer 2 already, so I set OCTSEL to 0.

    OC6CONbits.OCM = 0b110; // Output Compare Mode (OCM) is 6, which is PWM mode

Where does that come from? Again, the datasheet (beginning to see a pattern here?).

PIC32MZ - Output Compare Mode Select

<TL;DR>
What's the difference between modes 6 and 7? There are two extra PPS pins available to the Output Compare modules, called OCFA and OCFB (Output Compare Fault A and B). When a 0 is detected on these pins, the output of the Output Compare modules will be disabled. This can be useful in certain situations, but I've never actually had need to use this for any of my simple programs. Therefore, I use mode 6 which has the fault pin disabled and stay away from mode 7 which has it enabled.
</TL;DR>

Next, set the Output Compare Register (OCR) value:

    OC6RS = SYS_FREQ / 2 / 1000 / 8 / 2;    // We want half the length of PR2

This is very similar to setting up the Period Register of the timer (PR2 in my case). I've set it to half the value of PR2.

IMPORTANT: Although the Output Compare module compares the value in OC6R to PR2, when writing the value we must write to the OC6RS register. It will then move the value over to OC6R itself on the next cycle.

    OC6CONbits.ON = 1;      // Turn on Output Compare module 6 (OC6)

That's pretty self explanatory.

IMPORANT: The Output Compare modules are set up using Peripheral Pin Select, so if you don't set them up they won't work at all. For example, to set port C1 to Output Compare module 6 (OC6), you would do this:

    RPC1R = 0b1100; // Set RC1 to OC6

Remember, all of these PPS selections can be found in the PIC32MZ datasheet, currently in section TABLE 12-3: OUTPUT PIN SELECTION.

Using PWM to control an analog servo motor

I bought some PowerHD HD-1370A miniature servo motors from Banggood in order to make a very, very simple robot that can open and close its mouth. One of the things I always envied about Arduino users is that the code and libraries are all there for them. However, compared to the PIC32MZ the Arduino sucks in almost every way imaginable. Let's get this motor working with the PIC32MZ.

Servo motors usually require control signals to be sent in signals that are 20 milliseconds long. In general, the pulses themselves are between 1 and 2 milliseconds long, with 1 millisecond meaning turn as far clockwise as you can, 1.5 milliseconds meaning go to the mid-point (neutral) and 2 milliseconds meaning turn as far counter-clockwise as you can. The pulse is high for the duration and then for the rest of the 20 milliseconds it needs to go low again. This value of 1 to 2 milliseconds is very general and each motor has different requirements, so please always consult the motor's datasheet.

First, let's take a look at the datasheet for my motor:

PIC32MZ - PowerHD HD-1370A datasheet

The numbers I care about there are the degrees it can travel, the pulse width range of 800us to 2200us and the neutral position of 1500us. So, in theory, if I send a control pulse that is high for 800us it will rotate clockwise as much as it can and if I send a pulse that is high for 2200us it will rotate counter-clockwise as far as it can go.

OK, so what I need:

  • A timer that has a frequency of 50Hz (to give me 20ms / timer tick)
  • A way of sending out a pulse that shifts between high and low during the 20ms period
  • A way of setting the duty cycle (high/low time) of the control signal

Let's set up the timer first, using Timer 2. As always, let's check if this value fits in the Period Register (PR):

PR2 = SYS_FREQ / 2 / 1000 / 8 = 100,000,000 / 50 / 8 = 250,000

Nope. We need the value to be less than 65,536. A quick look at the possible pre-scaler values gives us 32, let's try plug that in:

PR2 = 100,000,000 / 50 / 32 = 62500

This works fine, so we need to set TCKPS to 0b101, which will give us a pre-scaler value of 32.

void PWM_init()
{
    T2CON   = 0x0;      // Disable timer 2 when setting it up
    TMR2    = 0;        // Set timer 2 counter to 0

    /* Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by 50 Hhz and then divided again by 32 for our chosen pre-scaler. */
    PR2 = SYS_FREQ / 2 / 50 / 32;

    // Set up the pre-scaler
    T2CONbits.TCKPS = 0b101; // Pre-scale of 32

    // Turn on timer 2
    T2CONbits.TON   = 1;

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

Now, let's calculate how to send a 1000us long high pulse. The Period Register value of 62500 represents the full 20ms long signal. Let's use that to work out the value we need to put into OC6RS:

(1 / 20) * 62500 = 0.05 * 62500 = 3125

There are ways to have it slightly more accurate in cases where the number is fractional, but for now let's try that out and see what happens:

/* in main() */
OC6RS = 3125;               // Move servo to 1ms position

Let's see what we get on the scope:

PIC32MZ - 20ms PWM signal

OK, excellent. The period of the signal is 20ms, let's zoom in on that pulse though and see if it's 1ms:

PIC32MZ - 1ms PWM pulse

Hooray, we have what we want. So plugging my servo motor's signal pin into this wire (along with giving it Vcc and Ground of course) will result in the servo moving to the position I want it to. You can even get it to move between positions easily now:

/* in main() */
while (1)
{
    OC6RS = 3125;       // Move servo to 1ms position
    delay_ms(1000);
    OC6RS = 6250;       // Move servo to 2ms position
    delay_ms(1000);
}

Remember, all servos are different and require different timing. Especially if you're like me and source them from AliExpress, Banggood and eBay. The motors found there are often clones and have slightly different timing to the originals. Good luck!

Here's the code

Tags: code, PWM, motors

Timers and Interrupts on the PIC32MZ

Using timers on the PIC32MZ

Timers overview

Timers are one of the things that the PIC32MZ does really well. It has 9 independent 16-bit timers, some of which can be combined to make 32-bit timers. It also has an awesome Output Compare module, which I'll cover next time with Pulse Width Modulation (PWM). All the timers work as follows:

  • On every PBCLK3 clock cycle, the value of the timer counter (TMR) is increased.
  • This value is compared to the Period Register (PR) value.
  • If the value is bigger or equal to PR, an interrupt is generated and the value of the counter is reset.

From this it can be seen that the most important registers for us are the Timer Register (TMR) and Period Register (PR).

IMPORANT: The timers on the PIC32MZ are all controlled by Peripheral Bus Clock 3 (PBCLK3), so whatever you set that to will directly affect all the timers.

<TL;DR>
16-bit? 32-bit? As you likely know, 16-bit unsigned variables can store a maximum value of 2^16 - 1, or 65535, and 32-bit unsigned variables can store 2^32 -1, or 4,294,967,295 (~4 billion). This can become very important when we are either wanting super precision or extremely low frequency timers, as I will show later.
For 32-bit timers, timers 2 and 3, timers 4 and 5, timers 6 and 7 and timers 8 and 9 can be combined to form the 32-bit timers.
</TL;DR>

Calculating the value to put into the Period Register (PR)

Say for example we want to generate a timer that runs at 10kHz. We calculate the period like this:
Period Register = Peripheral Bus Clock 3 Frequency / 10000.
In my examples, PBCLK3 runs at 100MHz, so this gives us: PR = 100,000,000 / 10,000 = 10,000.
10000 is much smaller than the maximum value of 65,535, so no problems there.
Now let's try that again but at 1kHz:
PR = 100,000,000 / 1,000 = 100,000.
Uh oh. 100,000 cannot fit into 65,535 so we're going to have the timer running at a frequency we don't expect. What can we do about this? Well, two things:

  • Change over to a 32-bit timer, which is overkill of the highest degree, OR
  • Use the timer's built-in pre-scaler

Using the pre-scaler

OK before I start this section, be careful which timer number you use. Timer 1 has some differences to Timers 2 to 9 and as such I recommend using Timers 2 to 9 unless you know what you're doing. In my example today I'll be using Timer 2. Anyway, the timer has a kind of built-in divider circuit called a pre-scaler. It works like this:

  • On every PBCLK3 clock cycle, the value of an internal counter is increased.
  • This value is compared to pre-scaler setting (TCKPS) in the Timer 2 Control Register (T2CON).
  • If the value is bigger or equal to TCKPS, then the internal counter TMR is incremented.

Examination of the TxCON register in the datasheet shows this:
PIC32MZ - Pre-scaler settings

So, bringing this back to my example of a 1kHz timer, I can now accomplish it as follows:

  • Set the pre-scaler to 8 (TCKPS = 011)
  • Calculate the period as follows: PR = 100,000,000 / 1,000 / 8 = 12500.

12500 fits neatly into a 16-bit register, so no problems there. Do be aware that some pre-scaler values will lead to fractional numbers, which will just be truncated and lead to inaccurate timing. For example, a pre-scaler value of 64 would lead to a required PR of 1562.5.

Setting up a timer

OK, phew. Theory out the way, let's see how to do this in code:

void init_timer2(int period)
{
    T2CON   = 0x0;      // Disable timer 2 when setting it up
    TMR2    = 0;        // Set timer 2 counter to 0

    /* Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by the frequency we want and then divided by 8 for our chosen pre-scaler. */
    PR2 = SYS_FREQ / 2 / period / 8;

    // Set up the pre-scaler
    T2CONbits.TCKPS = 0b011; // Pre-scale of 8

    // Turn on timer 2
    T2CONbits.TON   = 1;
}

Bam, that's it! It's incredibly simple to do. Of course, now the timer is running and that's great and all but we want to use the timer for something. For that, we need to discuss interrupts.

Interrupts on the PIC32MZ

The PIC32MZ is a fast processor but it is still a single core, single threaded processor. This is important because if we are running a while() loop and our timer keeps interrupting our code every 1/1000 times a second, the PIC32MZ will leave what we are doing in the while() loop, jump to the code for doing whatever we told it to do when the timer ticks, complete that, and then jump back to the while loop until it is interrupted again. These interruptions are called interrupts, funnily enough.

The PIC32MZ has a very deep interrupt system. In short, each interrupt can be assigned a priority from 1 to 7 and a further sub-priority of 0 to 3.
A higher interrupt priority gets handled first and a lower interrupt priority gets handled later. Further, a higher priority interrupt can actually interrupt a lower priority interrupt but the reverse is not true. Sub-priorities can be used when interrupts have the same pririoty, as a further way of sub-dividing which of these interrupts is more important.

First, before we begin, you should generally enable Multi Vectored interrupt mode. This will save you many headaches when working with interrupts and you're finding they don't work and you can't work out why. It basically tells the PIC32 to allow use of a different handler for each different type of interrupt (Timer 2, Timer 3, etc). Thankfully, this is very easy to do:

    INTCONbits.MVEC = 1;

The timer 2 interrupts have four important bits associated with them:

  • Timer 2 Interrupt Enable (T2IE)
  • Timer 2 Interrupt Flag (T2IF)
  • Timer 2 Interrupt Priority (T2IP)
  • Timer 2 Interrupt Sub-priority (T2IS)

Searching the infamous datasheet reveals that T2IE is in the IEC0 register, T2IF is in the IFS0 register and both T2IP and T2IS are in the IPC2 register. It is left as an exercise to the student to search the datasheet to find where the other timer interrupt bits are located :)

OK. let's re-visit that init_timer2() function from before, but this time let's enable the Timer 2 Interrupt with a priority of 3 and a sub-priority of 1.

void init_timer2(int frequency)
{
    T2CON   = 0x0;      // Disable timer 2 when setting it up
    TMR2    = 0;        // Set timer 2 counter to 0
    IEC0bits.T2IE = 1;  // Disable Timer 2 Interrupt

    // Set up the period. Period = PBCLK3 frequency, which is SYS_FREQ / 2, divided by the frequency we want and then divided by 8 for our chosen pre-scaler.
    PR2 = SYS_FREQ / 2 / frequency / 8;

    // Set up the pre-scaler
    T2CONbits.TCKPS = 0b011; // Pre-scale of 8

    IFS0bits.T2IF = 0;  // Clear interrupt flag for timer 2
    IPC2bits.T2IP = 3;  // Interrupt priority 3
    IPC2bits.T2IS = 1;  // Sub-priority 1
    IEC0bits.T2IE = 1;  // Enable Timer 2 Interrupt

    // Turn on timer 2
    T2CONbits.TON   = 1;
}

OK, that was a lot of explaining for very little code. There's one final step, writing the interrupt handler. Almost done, bear with me!

Writing an interrupt handler on the PIC32MZ

I want my timer interrupt handler to toggle pin RH0 every time it is called. Let's take a look at how I write this:

void __attribute__((vector(_TIMER_2_VECTOR), interrupt(ipl3soft), nomips16)) timer2_handler()
{
    IFS0bits.T2IF = 0;  // Clear interrupt flag for timer 2
    LATHINV = 1 << 0;   // Toggle pin RH0
}

OK, the code itself is very simple but hoo boy that declaration. Let's break it down:

  • __attribute__ tells the compiler we are going to give it specific instructions for how to handle this function.
  • The interrupt vector _TIMER_2_VECTOR is defined by the include file p32mz2048efh144.h. You can also use the IRQ number found in the datasheet directly, in this case 9.
  • The ipl3soft refers to Interrupt Priority Level 3 (yes, that's a lowercase L). If you want to know what the soft part means, refer to the <TL;DR> below.
  • nomips16 forces the compiler to generate code using the MIPS32 instruction set and not the MIPS16 instruction set. This is required for interrupts on the PIC32.
  • timer2_handler() is just what I named the function, you can name it whatever you want.

Inside the function itself, there are only two lines. The first line clears the interrupt flag for timer 2. If you do not clear this flag, the interrupt handler will immediately and repeatedly get called again until it is cleared. It is recommended that you clear this flag immediately, and that your code inside an interrupt handler does not take too long. Keep interrupt handler code short and simple, don't write files or anything inside an interrupt because by the time the file writing is done the PIC32MZ will have missed the timing of several interrupts and things will go pear-shaped. And that's it, interrupt handler done!

Let's take a look with an oscilloscope to see if this gives us what we want:

PIC32MZ - 1kHz timer scope verification

And there it is, just what we expect. The signal is high for 1ms, low for 1ms, high for 1ms, etc, which means it's changing at a rate of 1kHz. Unlike the FatFs disasters, it seems to have worked out this time! :)

<TL;DR>
There is a rather large problem with interrupts in general. The processor needs to temporarily pause what it was doing, jump to the interrupt handler and then go back to what it was doing. This means, of course, that it needs to first save what it was doing so it can go back to it later. In case of the PIC32MZ, this means it needs to save the contents of some important registers before it can actually call the interrupt handler. This delay is called interrupt latency. For almost all of the stuff I do, which is not time-critical, this delay doesn't make much difference but for time critical stuff it can be a huge problem. The PIC32MZ combats this by having a Shadow Register Set (SRS) that you can use to minimize this latency. Today's post is long enough, however, perhaps in the next post. Or you're welcome to Google it for yourself :)

To summarise:

  • Setting it to soft forces it to use software context switching, i.e. manually save all the registers. This is the slowest option.
  • Setting it to srs forces it to use the shadow register set, which is much faster. It will also crash your program if you haven't set it up.
  • Setting it to auto tells it to choose because software context switching and using the shadow regiter set. I've had issues with this when I hadn't set up the shadow register set so be warned!

</TL;DR>

Here's the code

Tags: code, timers, interrupts