Driving servos with the help of interrupts

Servos are electrical motors that depending on the steering signal move into a certain position. A servo typically has three connections: the negative voltage power, the positive voltage power (5 V) and the steering signal. The Dwengo board has two connectors servo1 and servo2 that allows to directly connect servos to it. On this page we explain in detail how the steering signal for a servo can be generated in an efficient way with the PIC18F4550. Dwengo provides a library dwengoServo.h that hides these details from the user.

The desired position of the servo is send in the form of a PWM signal. PWM stands for Pulse-Width Modulation. A PWM signal is an electrical signal of which the voltage periodically generates pulses. The width of these pulses determines the servo position. So is we change the width of the pulses, we will change the position of the servo. This is illustrated in the figure below. The PWM signal for steering the servo typically has a period of 20 ms and the width of the pulse varies between 0.7 and 2.3 ms.

PWM met servo-posities

The principle: timer interrupts

We could generate the PWM signal for the servos by using delays as shown in the code below in order to create a PWM signal with a pulse-width of 1 ms and a period of 20 ms. However, this is very inefficient way, since the microcontroller spends most of its time waiting and doing no useful work. It is much better in this case to make use of a Timer and its corresponding interrupt.

  1. while(TRUE) {
  2. SERVO1 = 1;
  3. delay_ms(1);
  4. SERVO1 =0;
  5. delay_ms(19);
  6. }

A timer is a piece of hardware on the chip. One can setup the timer to run out after a certain amount of time. After the timer is setup, it starts counting down and the microcontroller can continue with some other work. The moment the timer runs out, it sends an interrupt to the microcontroller. This will stop the normal execution and first executes the Interrupt Service Routine (ISR) before continuing the normal execution.

We ca use this mechanism to generate a PWM signal with the same characteristics as the code shown above:

  • Set in the beginning of the main loop the servo1 equal to 1 and set the timer in at 1 ms.
  • Write an ISR that:

-- set servo1 equal to 0 and the timer to 19 ms if servo1 is equal to 1.
-- set servo1 equal to 1 and the timer to 1 ms if servo1 is equal to 0.

The C code

Below you find the C code that, with the use of interrupts, generates a PWM signal with a pulse-width of 1 ms and a period of 20 ms. We will now explain step by step what all this code means.

  1. #pragma config PLLDIV = 5 // Divide by 5 (20 MHz oscillator input)
  2. #pragma config FOSC = HSPLL_HS // HS oscillator, PLL enabled, HS used by USB
  3. #pragma config IESO = OFF // Oscillator Switchover mode disabled
  4. #pragma config PWRT = OFF // PWRT disabled
  5. #pragma config BOR = OFF // Brown-out Reset enabled in hardware only (SBOREN is disabled)
  6. #pragma config WDT = OFF // HW Disabled - SW Controlled
  7. #pragma config WDTPS = 32768 // 1:32768
  8. #pragma config MCLRE = ON // MCLR pin enabled; RE3 input pin disabled
  9. #pragma config LVP = OFF // Disable low-voltage programming
  10. #pragma config CCP2MX = ON // CCP2 is multiplexed to RC1 and not to RB3
  11. #pragma config PBADEN = OFF // PORB digital IO on powerup
  12.  
  13. #include <p18f4550.h>
  14.  
  15. #pragma interrupt ISR
  16. void ISR() {
  17. if (PIR1bits.TMR1IF == 1) { // TIMER1 interrupt?
  18. if (PORTBbits.RB5 == 1) {
  19. PORTBbits.RB5 = 0;
  20. TMR1L = 37036 & 0x00FF;
  21. TMR1H = (37036 & 0xFF00) >> 8;
  22. } else {
  23. PORTBbits.RB5 = 1;
  24. TMR1L = 64036 & 0x00FF;
  25. TMR1H = (64036 & 0xFF00) >> 8;
  26. }
  27. PIR1bits.TMR1IF = 0; // Reenable TIMER1 interrupt
  28. }
  29. }
  30.  
  31. #pragma code high_vector=0x08
  32. void high_vector() {
  33. _asm
  34. goto ISR
  35. _endasm
  36. }
  37. #pragma code
  38.  
  39. void main(void) {
  40.  
  41. // Initialise servo pin
  42. TRISBbits.TRISB5 = 0; // Set RB5 as output
  43. PORTBbits.RB5 = 1; // Initialise with 1
  44.  
  45. // Initialise prescaler to 8
  46. T1CONbits.T1CKPS0 = 1;
  47. T1CONbits.T1CKPS1 = 1;
  48.  
  49. // Set TIMER1 to 1 ms
  50. TMR1L = 64036 & 0x00FF;
  51. TMR1H = (64036 & 0xFF00) >> 8;
  52.  
  53. T1CONbits.TMR1ON = 1;
  54.  
  55. // Allow TIMER1 interrupts
  56. INTCONbits.GIE = 1;
  57. INTCONbits.PEIE = 1;
  58. PIE1bits.TMR1IE = 1;
  59.  
  60. PIR1bits.TMR1IF = 0;
  61.  
  62. // Wait forever
  63. while (1) {
  64. }
  65. }

Initialization of pin servo1

Since we want to generate the PWM signal on the connector servo1 of the Dwengo board, we have to setup the signal pin of this servo connector, RB5, as an output pin. We also set the output value equal to 1. This is done with the following code (line 42-43)

  1. TRISBbits.TRISB5 = 0; // set RB5 as output
  2. PORTBbits.RB5 = 1; // initialize with 1

Initialization of TIMER 1

As explained before, the timer is a piece of hardware on the chip of the microcontroller that generates an interrupt after a preset time. To setup this time we need to setup several registers with the correct value. Instead of directly setting the time, we setup two values:

  • The number of counts before the timer runs out and as such generates an interrupt
  • The time between two counts.

We set the time between two counts of TIMER 1 to 8 x 83.33 ns = 666.66ns. So the timer will count every 8 instruction cycles (=83.33 ns). This is achieved by setting the prescaler of the timer equal to 8. If you read the PIC18F4550 data sheet External link carefully (section 12.0, bits 5-4), you will see that we need to set the bits T1CKPS1 and T1CKPS0 of the register T1CON equal to 1.

  1. T1CONbits.T1CKPS0 = 1;
  2. T1CONbits.T1CKPS1 = 1;

With each count (in our case every 666.66 ns) the 16 bit register TMR1 is incremented with 1. When we reach the maximum value of the register, the timer sends an interrupt and starts counting again from 0. The maximum value of 16 bit register is 65536. Normally the timer runs out every 65536*666.66 ns = 43.69 ms. However, we can also set the timer to run out after a preset time t. We do this by writing a value w in the TMR1 register, so that it will take only t seconds before the register reaches 65536. We can calculate this value w as follows:

w = 65536 - t / 666,66 ns

For a time of 1 ms the value w = 65536 - 1 ms/ 666.66 ns = 65536 - 1500 = 64036. For a time of 19 ms we need a value w = 65536 - 19 ms/ 666.66 ns = 65536 - 28500 = 37036. We want that the pulse is 1 ms long, so we need to write the value 64036 in the TMR1 register. Since the PIC18F4550 is a 8 bit microcontroller we have to write a 16 bit register in two steps. First we write the least significant 8 bits of the register (TMR1L) and next we write the 8 most significant bits of the register (TMR1H). In C code we get the following:

  1. TMR1L = 64036 & 0x00FF;
  2. TMR1H = (64036 & 0xFF00) >> 8;

We also have to activate TIMER 1. This is done by setting the TMR10N bit of the T1CON register high.

  1. T1CONbits.TMR1ON = 1;

Initialization of interrupts

In the next step we setup the microcontroller in such a way that it will execute the ISR when the TIMER1 interrupts occurs. To do this we need to take the following steps:

  • Allow interrupts by setting the Global Interrupt Enable bit (GIE) on 1;
  • Allow interrupts from peripherals such as TIMER 1 by setting Peripheral Interrupt Enable bit (PEIE) on 1;
  • Allow interrupts from TIMER 1 by setting TMR1IE bit on 1 and the TMR1IF bit on 0.

More details can be found in the PIC18F4550 data sheet External link.

  1. INTCONbits.GIE = 1;
  2. INTCONbits.PEIE = 1;
  3. PIE1bits.TMR1IE = 1;
  4. PIR1bits.TMR1IF = 0;

Interrupt Service Routine

The Interrupt Service Routine (line 15-29) is called each time an interrupt occurs, so not only when an interrupt from TIMER1 occurs. Hence, we need to check at the beginning of the ISR if the interrupt was caused by TIMER1 (line 17). Next we can run the code to handle the TIMER1 interrupt (line 18-27). If the servo1 pin is high (1), we set the servo1 pin low (0) and set the timer on 19 ms. If the servo1 pin is low (0), we set the servo1 pin on high (1) and set the timer on 1 ms. At the end of the ISR routine (line 27) we set the TMR1IF bit to 0 in order to allow future TIMER1 interrupts again.

To conclude we discuss lines 31-37. If an interrupt occurs, the microcontroller reacts on this by not executing the following instruction of the normal execution, but the instruction of a predefined address. In the case of the PIC18F450 this address is 0x08. We have to make sure that the ISR is executed when an interrupt occurs. Lines 31-37 enable this by placing a jump instruction on address 0x08 to the ISR.

Syndicate content