Pulse Width Modulation (PWM) and servos on the PIC32MZ post

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

Categories: pic32

Tags: code, PWM, motors