Four (4) independent PWM-signals generated with one PIC

Every self-respecting engineer has at least one moment in his or her life when it seems like the right thing to do is build flying stuff. In the case of me and a few friends, this means that we are going to do an attempt at creating our own quadrocopter. We quickly started looking around the internet, and placed an order at http://www.hobbyking.com/ (I will describe the contents of this order in a later post once it has shipped to my doorstep).

Because the shipping would take a few days, and another property of the self-respecting engineer is that he does not have any form of patience when it comes to building stuff. We decided that we would already start designing the control circuitry, which we will try to do from scratch. A first step in this is finding a way to control the speed of the motors.

The Signal

Quadrocopters are built with brushless DC motors. A common mistake is to assume that these are conventional DC-motors, and can be driven as such, which is far from true!! Actually, a brushless DC motor is a permanent magnet AC motor with a block waveform. I won't go into detail, but this is important stuff! Because of this AC-block waveform we can't just drive this motor with a normal H-bridge, other circuitry is required. It would be very possible to build this ourselves, but the challenge is pretty low and because this is part of the power-circuit, the risk of introducing power-loss is too high. This makes that we use what the industry has provided for us: the ESC (Electronic Speed Controller). We can just hook up our motor and our battery to the ESC. If this is driven with a specific PWM signal, the speed of the motor will vary. This is what the PWM signal looks like:

First send a 1 ms pulse, then send the second pulse that controls the speed (max. speed = 1ms, min. speed = 0ms) and then wait until the total period is about 10ms. The waiting time can be longer than 10 ms because it would be hard to do this with RC otherwise. It is only the pulse length that is important. From this information, we know that we have to generate a PWM-signal of 100Hz (period 10ms) with a duty cycle that is variable between 10% and 20%. We will have to be able to do this 4 times independently, because a quadrocopter has four motors that have to be able to operate at different speeds (duh!).

Prototype

As the title said, we created this PWM-controller using a PIC microcontroller. The one we chose was a very small one: a PIC16F684 with 128 bytes of RAM and 2048 words of flash. Because we don't have any RC-setup yet, the inputs that control the motor speed are 4 potentiometers that are read in with an analog multiplexer (74HCT4051). This saves us a pin on the MCU and makes it a little easier to program the ADC unit properly. Also when we want to add accelerometer or gyro modules later with analog signals, we can easily do this without losing pins (except for one extra adres pin ofcouse!). The full schematic looks like this:

Here's a picture of the prototype:

The four LED's are at the output pins (where the ESC's will be attached to) and give some visual feedback during prototyping (the intensity of a LED changes when the duty cycle is modified, duh!).

We could have chosen a PIC that has 4 independent PWM channels like the PIC18F4x31. Why didn't we? Because we believe that this is one of the simplest parts of making a quadrocopter and it shouldn't have any impact on the choice of your hardware. It's also a nice exercise in efficiency to work with low resources. If we want to cram the full quadrocopter control-software in a PIC one day, we might want to start thinking efficient from day one. The code I provide here is not 100% optimal, but I did my best (let me know if you have any improvements!).

Program Structure

The hardest thing about writing the code was finding a way of outputting the 4 PWM signals with varying duty cycles at the same time . It is possible that motor 1 had to go faster than motor 2 at first (motor 1's pulse is longer than motor 2's pulse), but when the direction ischanged, motor 2 has to go faster than motor 1. This made it a little more awkward to generate the 4 PWM's at the same time. A little creativity though, easily solved this problem by sorting the pulses from short to long so that they could be turned off sequentially. Because there are 3 obvious parts to the signal shown above, I decided to cut my code into three big pieces. This is how the program was organized:

Part 1: 0 - 1 ms

  • Set all output pins to high.
  • Read the 4 potentiometers and convert them to digital values using the internal ADC of the PIC. The values are stored in the ADCValues table.
  • Sort the ADCValues table from low to high and keep track of which output corresponds to which value (whichMotor).
  • Wait until the first millisecond has passed

Part 2: 1 - 2 ms

  • Wait until first output pin has to be turned off, and then turn it off.
  • Do this for the other 3 pins as well. - Wait until the second millisecond has passed.

Part 3: 2 - 8 ms

  • Wait until 8 ms have passed.

The C program

I make extensive use of the internal timers (timer 1) of the microcontroller because this gives a lot of control over the actual time passed, and you can do other stuff in the meantime instead of just waiting like with a delay function. The time that passes for a value of the TMR1-register can be easily calculated with: time = 1/OscValue * 4 * Prescaler * (65536 - TMR1). A very useful tool for this is the PIC timer calculator. As you can see in the code underneath, the remaining program has to be put in the two while loops. I indicated this with "// Do Something" in the comment.

I make use of the MPLABX IDE and the Hi-Tech C Lite compiler.

#include <pic.h>
#include <pic16f690.h>
#include <math.h>
 
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_OFF & MCLRE_OFF & CP_OFF);
 
#define _XTAL_FREQ 20000000
 
/* Function prototypes */
void ADC(void);
void main(void);
 
/* Global variables */
unsigned char ADCValues[4];
unsigned char whichMotor[4];
unsigned char help;
 
static int accData[3], gyrData[3]; // AccX ; AccY ; AccZ ; GyrX; GyrY; GyrZ
//SPid pitchPID, rollPID;
static int pitch = 0, roll = 0;
 
/* Main program */
void main(void)
{
    unsigned char i, j;
    unsigned char PORTAValue = 0b00000000, PORTCValue = 0b00000000;
 
    // Init
    TRISA = 0b00000001;     // RA0 input (Analog), RA1 & RA2 output (MUX adres)
    TRISC = 0b00000000;     // Port C output (PWM Signals)
 
    ADCON0 = 0b00000001;    // AD conversie init
    ADCON1 = 0b00000000;
 
    PEIE = 1;               // Peripheral interrupt enable
    GIE = 1;                // Global interrupt enable
    TMR1IE = 1;             // Timer 1 interrupt enable
    T0IE = 1;
 
    TMR1IF = 0;
 
    T1CON = 0b00000000;     // Timer 1 prescale 1:1
 
    // Infinite loop
    while(1)
    {
        // ------------------------------------------------------------------ //
        // -------------------------- MS 0 to MS 1 -------------------------- //
        // ------------------------------------------------------------------ //
 
        // Set all the pins high to start the PWM
        PORTCValue = 0b00001111;
        // Keep track of current state of the ports in a register to walk around
        // the RMW-problem
        PORTC = PORTCValue;
 
        // Start Timer 1 ms (time = 1/OscValue * 4 * Prescaler * (65536 - TMR1))
        TMR1 = 60536;   // 1 ms with prescaler 1:1 and freq 20MHZ
        TMR1ON = 1;
 
        // Get ADC value of all input POT'S
        i=0;
        for(i; i < 4; i++)
        {
            // Set MUX adres on RA1 and RA2
            // without affecting current state of PORTA (READ MODIFY WRITE)
            PORTAValue &= 0b11111001;
            PORTAValue |= (i << 1);
            PORTA = PORTAValue;
 
            __delay_us(1); // Delay here to wait until MUX value is on the pin
            ADC();
            (ADRESH <= 250)? ADCValues[i] = ADRESH : ADCValues[i] = 250;
        }
 
        // Initialise the whichMotor table
        i = 0;
        for(i; i<4; i++)
            whichMotor[i] = i;
 
        // Sort the ADC values from low to high
        // (this is the order in which they will be turned off)
        i=0;
        for(i; i<4; i++)
        {
            j = i+1;
            for(j; j < 4; j++)
            {
                if(ADCValues[j] < ADCValues[i])
                {
                    help = ADCValues[i];
                    ADCValues[i]=ADCValues[j];
                    ADCValues[j]=help;
 
                    help = whichMotor[i];   // Keep track of output pins
                    whichMotor[i] = whichMotor[j];
                    whichMotor[j] = help;
                }
            }
        }
 
        // Calculate the values that have to be loaded in the timer
        unsigned int delayValues[5];
        i = 0;
        delayValues[0] = 65535 - ADCValues[0]*20;
        for(i; i<3; i++)
        {
            // time = 1/OscValue * 4 * Prescaler * (65536 - TMR1)
            // ADC value is scaled to 1/4 of 1000 (value * 4)
            delayValues[i+1] = 65535 - (ADCValues[i+1] - ADCValues[i])*20;
        }
        delayValues[4] = 60535 + ADCValues[3]*20; // remaining time to 1 ms
 
        // Wait until the first ms is passed
        while(!TMR1IF)  // Wait until the timer overflows
        {
            // DO something
        }
        TMR1IF = 0;     // Must be cleared in the software
        TMR1ON = 0;
 
        // ------------------------------------------------------------------ //
        // -------------------------- MS 2 to MS 3 -------------------------- //
        // ------------------------------------------------------------------ //
 
        // Turn off the outputs in the right order: ~ is binary invert
        // Wait until first output has to be turned off
        // whichMotor holds the index of the current output pin
        // (bitshift inverted 1 this many places to turn that output off)
        i = 0;
        for (i; i<4; i++)
        {
            TMR1 = delayValues[i];
            TMR1ON = 1;
            PORTCValue &= ~(0b00000001 << whichMotor[i]);
            while(!TMR1IF);
            TMR1ON = 0;
            TMR1IF = 0;
            PORTC = PORTCValue;
        }
 
         TMR1 = delayValues[4];
         TMR1ON = 1;
         while(!TMR1IF);
         TMR1ON = 0;
         TMR1IF = 0;
 
        // ------------------------------------------------------------------ //
        // -------------------------- MS 3 to MS 10 ------------------------- //
        // ------------------------------------------------------------------ //
 
        // Start Timer 8 ms
        TMR1 = 25536;   // 8 ms with prescaler 1:1 and freq 20MHZ
        TMR1ON = 1;
        while(!TMR1IF)
        {
            // Do shit - read sensor or whatever
        }
        TMR1IF = 0;
        TMR1ON = 0;
    }
}
 
void ADC(void)
{
    GO_DONE = 1;
    while(GO_DONE);
}

Results

It has to be said that this code is not perfect yet. Because stopping & starting the timers and writing to PORTC all happens outside the timer, a small error is made here. Of course this could be compensated for, but this would make the code more complex, which is not necessairy.

Here are some pictures and a video to show the working circuit.



Here is a video of our PWM-controller driving two motors (I was too lazy to screw on all four motors because I will have to take them off soon anyway ...):