PIC Hardware PWM for servo control

Status
Not open for further replies.

1krr

New Member
Hi all. Newb here. I've been teaching myself 'pic' for a couple of weeks not and have done the whole make LEDs blink and such. Next on the learning curve is controlling a servo with the pic. I am using the Microchip 44 pin demo board that houses a 16f917. The 917 has two on board CCP modules, one of which I wanted to use to drive a servo.

In searching through this site, I've notice a few references suggesting that the PWM module is not well suited for driving a servo for a number of reasons. In addition, being new, I've been working hard to wrap my mind around the configuration and operation of the CCP for PWM mode. I would also like to use the other CCP to pulse a solenoid.

I would love to use the CCP as it would allow me to free up the pic for other tasks while allowing me to control all the peripherals from a single device. However if it's not well suited, I don't want to spend the learning time for this project.

Can someone give me a little insight on whether or not the PWM module is effective in servo control or should I build an entirely software based solution?

Thanks in advance!!
 
Personally I don't consider it very suitable, it's not intended for that type of use - there are ways to use it, by altering it every pulse - it's been described here before. But mostly servo control is done via software.
 
Thanks Nigel, that was what I needed to know. The software seems straight forward but I'm going to have to learn how to network pics now!
 
Just because it's software doesn't mean it has to take total control of the hardware. I have no idea what timers and interrupts the 16f977 has, but it should be easy enough to create your own interrupt based servo control and take little resources.

Actually I have a nice paper on implementing 8/16 independant servo controls on a uC with little overhead using the bitwise method. It does take a bit of memory, though.

EDIT: Actually, point is moot after Mike's post above.
 
If you want the maximum resolution out of your PIC for generating servo signals it's best to do the signal generation in software. Start the pulse and then using two or three calibrated delay routines you can stop the pulse within a single clock cycle of the desired length. This will give you a pulse resolution of 62.5 nano seconds on a 16mhz system clock. This gives you about 16,000 position in the servo's standard 1-2ms range Or a little less than 14 bits of resolution.

Using a single basic timer to keep track of the delay loop you can allow your program to process other information during the bulk of the delay routine.
 
Last edited:

With a 16MHz clock, a single cycle takes 250nS. The code I linked to above gives a resolution of 400nS and uses less than 1% of the processors time.

Mike.
Edit, the above code could work at 200nS resolution but wouldn't be as elegant.
 
That's nice "special event trigger" code Mike.

You could also use CCP "compare" mode and the let the CCP module set and clear the CCP1 pin. This would eliminate the one or two cycle pulse width "jitter" that may result from toggling the pin manually in the ISR. It's also somewhat immune to "jitter" that might be caused by temporarily disabling interrupts or by servicing other interrupts because you have plenty of time after the CCP interrupt (500-usecs to 1000-usecs minimum) to service it and setup the next "compare" values.

Code:
void isr_hi ()
{ if (PIR1bits.CCP1IF == 1)             // CCP1 interrupt?
  { 
    if (CCP1CONbits.CCP1M0 == 0)        // CCP1 is hi, make it go
    { CCP1CONbits.CCP1M0 = 1;           // lo next match interrupt
      CCPR1H += (Pulse/256);            // setup Pulse on time
      CCPR1L += (Pulse%256);            // match/interrupt value
    }
    else                                // CCP1 is lo, make it go
    { CCP1CONbits.CCP1M0 = 0;           // hi next match interrupt
      CCPR1H += ((20000-Pulse)/256);    // setup Pulse off time
      CCPR1L += ((20000-Pulse)%256);    // match/interrupt value
    }
    PIR1bits.CCP1IF = 0;                // clear CCP1 interrupt flag
  }
}
An Assembly language version of the ISR driver code might look something like this;

Code:
;
;  save main program context
;
        movwf   W_ISR           ; save W-reg                      |B?
        swapf   STATUS,W        ; doesn't change STATUS bits      |B?
        movwf   S_ISR           ; save STATUS reg                 |B?
        clrf    STATUS          ; force bank 0                    |B0
        movf    FSR,W           ;                                 |B0
        movwf   F_ISR           ; save FSR                        |B0
;
;  test CCP1 interrupt flag bit
;
        btfss   PIR1,CCP1IF     ; CCP module interrupt?           |B0
        goto    ISR_XIT         ; no, branch, else                |B0
;
;  setup next CCP1 pin transition and compare interrupt timing
;
        btfsc   CCP1CON,CCP1M0  ; is CCP1 pin hi or lo?           |B0
        goto    Pulse_Lo        ; lo, branch, else                |B0
;
;  pulse is hi so setup CCP1 to go lo on Pulse "on-time" match
;
Pulse_Hi
        bsf     CCP1CON,CCP1M0  ; setup CCP1 to go lo next match  |B0
        movf    PulseLo,W       ;                                 |B0
        addwf   CCPR1L,f        ; CCPR1L += (Pulse)%256           |B0
        movf    PulseHi,W       ;                                 |B0
        skpnc                   ;                                 |B0
        incf    PulseHi,W       ;                                 |B0
        addwf   CCPR1H,f        ; CCPR1H += (Pulse)/256           |B0
        goto    Wrap            ;                                 |B0
;
;  pulse is lo so setup CCP1 to go hi on pulse "off-time" match
;
Pulse_Lo
        bcf     CCP1CON,CCP1M0  ; setup CCP1 to go hi next match  |B0
        movf    PulseLo,W       ;                                 |B0
        sublw   low  d'20000'   ;                                 |B0
        skpc                    ; borrow? no, skip, else          |B0
        decf    CCPR1H,f        ;                                 |B0
        addwf   CCPR1L,f        ; CCPR1L += (Period-Pulse)%256    |B0
        skpnc                   ;                                 |B0
        incf    CCPR1H,f        ;                                 |B0
        movf    PulseHi,W       ;                                 |B0
        sublw   high d'20000'   ;                                 |B0
        addwf   CCPR1H,f        ; CCPR1H += (Period-Pulse)/256    |B0
;
;  clear CCP1 interrupt flag
;
Wrap
        bcf     PIR1,CCP1IF     ; clear CCP interrupt flag bit    |B0

;
;  restore main program context
;
ISR_XIT
        movf    F_ISR,W         ;                                 |B0
        movwf   FSR             ; restore FSR                     |B0
        swapf   S_ISR,W         ;                                 |B0
        movwf   STATUS          ; restore STATUS                  |B?
        swapf   W_ISR,f         ; don't screw up STATUS           |B?
        swapf   W_ISR,W         ; restore W-reg                   |B?
        retfie                  ; return from interrupt           |B?
 
Last edited:
Hi Mike,

That C code certainly looks a lot simpler - I might have to have a play with that. Which compiler is it?

Also, you may have a slight bug in your asm code. Because you write the low byte first, you could get a match before the high byte is written. Something like incrementing the high byte near the beginning of the code should stop that.

Mike.
 
That's the Microchip C18 compiler for the 18F' devices.

Yes, sorry, the interrupt flag should probably be cleared at the end of the driver code to clear a false trigger caused by modifying the CCPR1L:CCPR1H values. I've corrected both code examples.

Mike
 
Last edited:
Status
Not open for further replies.
Cookies are required to use this site. You must accept them to continue using the site. Learn more…