Interrupts, driving a servo: a light tracker

In this tutorial we explain step by step how to generate a PWM signal (Pulse Width Modulation) using a timer driven interrupt routine. This PWM signal can for example be used to drive a servo. Besides some extra knowledge about the microcontroller you will have a working light tracker, after completing this tutorial!

Requirements

  1. One Dwengo board
  2. One Dwengo programmer
  3. Accompanied cables
  4. Two phototransistors accompanied with two 22 kOhm resistors
  5. Optionally a Dwengo breadboard in order to easily build your analog circuit and some wires
  6. A servo motor
  7. A soldering iron and some tin to build the sensor

PWM signals

PWM stands for Pulse Width Modulation and is way to encode information. In order to do this, the width of block wave is adjusted (modulated). An example of this is given in the figure. When we want to use a PWM signal as power, this techniques can generate states between completely off and completely on. In other words, the average voltage can be gradually regulated. On the other hand the width of the block wave pulse can also be used to encode information. The PWM signal can be used as a control signal for an electrical component.

PWM with  servo positions

In this tutorial we use a PWM signal to adjust the position of a servo. For servos one typically uses a block wave with a period of 20 ms. The width of a pulse is typically between 0.7 ms and 2.3 ms and determines the the position of the servo as indicated in the figure.

The microcontroller that is used in the Dwengo board has a build-in PWM module that enables us to generate a PWM signal effortless. This module is connected to the motor driver of the Dwengo board, so that it becomes easily to control the voltage of a motor. This is explained in the tutorial about the light eating robot. Of course you can use the build-in module to drive one of your servos. However, to be able to control the voltage for a motor as well as adjusting the position of a servo, we provide an alternative method to generate a PWM signal using timer interrupts.

Timer interrupts

So far in the previous tutorials we wrote programs that are executed sequential. This allows you to write a log of fun programs, but you are limited by the sequential execution of your program. Sometimes you want to execute small tasks without bothering the main loop of your program. This can be resolved by using interrupts.

The microcontroller provides interrupts a (in hardware build-in) mechanism to react on events that happen while the main loop of your program runs. A typical example of such events are push buttons that are pressed, analog sensors that reach a certain value, a timer that runs out, ...

Furthermore the microcontroller also has a build-in mechanism to keep track of the time. This mechanism is called a timer and on fixed intervals increases the value of a register. The update speed can be set in software. Most microcontrollers provide an interrupt the moment the register reaches is maximum value. It is this mechanism that we will use to generate a PWM signal.

Driving a servo

The microncontroller can be programmed in such a way that he performs a certain task when an interrupt occurs. For this our program has to be extended with an interrupt handler. The programmer, you in this case, has to make sure that this interrupt handler is written as efficient as possible, in order to disturb the main loop as little as possible. After all the main loop is interrupted each time an interrupt occurs. For the same reason it is a good idea to think in advance about the interrupts you want to use and not to exaggerate with them.

Libraries, macros and global variables

Besides loading the libraries and configuring the configuration bits, we first define several macros that will make the code more readable and define several global variables. That is a variable that is also visible outside the main loop.

  1. #define TRUE 1
  2. #define FALSE 0
  3. #define MAX_TIMER_SERVO 64536 // maximal left position
  4. #define MIN_TIMER_SERVO 62086 // maximal right position
  5. #define SERVO_STEP 10 // defines servo precision
  6. #define THRESHOLD 30 // defines light sensitivity
  7.  
  8. unsigned int timerServo; // global variable

Interrupt handlers

Next we add the prototypes for the interrupts handlers.

  1. void YourHighPriorityISRCode();
  2. void YourLowPriorityISRCode();

Note that we define two interrupt handlers. As the name of the function indicates we provide one interrupt handler with high priority and one with low priority. This becomes increasingly important when you have several types of interrupts that are generated from different hardware resources. By assigning them to some handler you can indicate the priority which is important when you have two interrupts at the same time. In this tutorial this is of no importance and we will only write code for the interrupts handler with the high priority.

Next we define the actual interrupt handlers. For this we use the word pragma interrupt and pragma interruptlow to indicate that the piece of code has to put in the correct place of program memory. Typically this address is different from the address where the main loop of the program comes. When an interrupt occurs, the microcontroller will start executing the code that starts from these addresses. Apart from that, the interrupt handlers need to be mapped on the right place in the data memory. This is done by setting the high_vector and low_vector.

  1. #pragma code high_vector=0x08
  2. void high_vector() {
  3. _asm
  4. goto YourHighPriorityISRCode
  5. _endasm
  6. }
  7. #pragma code
  8.  
  9. #pragma code low_vector=0x18
  10. void low_vector() {
  11. _asm
  12. goto YourLowPriorityISRCode
  13. _endasm
  14. }
  15. #pragma code
  16.  
  17. // interrupt handler routines
  18. #pragma interrupt YourHighPriorityISRCode
  19. void YourHighPriorityISRCode() {
  20. if(PIR1bits.TMR1IF == TRUE) { // check interrupt flag: from timer?
  21. if(PORTBbits.RB5 == 0) {
  22. TMR1H = (timerServo & 0xFF00) >> 8; // keep most significant byte
  23. TMR1L = timerServo & 0x00FF; // keep least significant byte
  24. PORTBbits.RB5 = 1; // high pulse
  25. } else {
  26. TMR1H = 0x8A; // timer 1 interrupt after 83.333ns*8*(65536-35536) = +/- 20ms
  27. TMR1L = 0xD0; // 35536 = 0x8AD0
  28. PORTBbits.RB5 = 0; // low pulse
  29. }
  30. PIR1bits.TMR1IF = FALSE; // reenable TMR1 interrupt
  31. }
  32. }
  33. #pragma interruptlow YourLowPriorityISRCode
  34. void YourLowPriorityISRCode() {
  35.  
  36. }

If you analyze the code of the function YourHighPriorityISRCode(), you recognize the following plan:
1. Look where the interrupt comes from, for this you have to check the different interrupt flags of the different resources of the microcontroller that can generate an interrupt. We only use the timer1 interrupt, so we only check the interrupt flag of timer1.
2. Execute the code that belongs to this interrupt, this code should be written as short and efficient as possible. In our case this means setting pin RB5 high or low depending on the state in which RB5 was. Next the timer1 register is setup correctly. This is discussed in more detail when we describe the main loop. Note that we use the global variable timerServo that is setup in the main loop.
3. Reactivation of the interrupt. In our case the reactivation of the timer1 interrupt is done by setting the correct register to 0.

Initialization

In the main loop we first create a couple of variables in which we will store the sensor values of the left and right light sensor. We also store the previous values as well. In addition to this we configure the ADC module of the microcontroller.

  1. void main(void) {
  2. int rightSensor, leftSensor, previousRightSensor, previousLeftSensor;
  3.  
  4. /*
  5.   ADC_FOSC_64: conversion clock, table 21-1, 1TAD = 1,33 us (has to be between 0,7 us and 25 us)
  6.   ADC_RIGHT_JUST: least significant bits
  7.   ADC_6_TAD: acquisition time, 6 TAD's used for good conversion, minimal 1,4 us required
  8.   ADC_INT_OFF: no adc-interrupts
  9.   ADC_VREFPLUS_VDD and ADC_VREFMINUS_VSS: use PIC ground and source as voltage reference
  10.   0b1010: pins AN0-AN4 configured as analog input pins
  11.   */
  12. OpenADC(ADC_FOSC_64 & ADC_RIGHT_JUST & ADC_6_TAD,
  13. ADC_CH0 & ADC_INT_OFF & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS,
  14. 0b1010); // Configuring ADC

We decide to generate the PWM signal on pin RB5. This pin is connected with the servo1 connector on the Dwengo board. For this reason we configure pin RB5 as an output and initialize the pin with 0.

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

We assign the value 62086 to the global variable timerServo . It will become clear later on where this value comes from. It is important to remember that we will determine the position of the servo with this variable.

  1. timerServo = MIN_TIMER_SERVO; // maximal right

Next we set the necessary registers to activate the build-in timer1 module with the corresponding interrupt. It begins with setting timer1 interrupt flag to 0:

  1. PIR1bits.TMR1IF = 0;

Since earlier on we made a difference between the occurrence of low and high priorities for interrupts, we activate the interrupt priorities with the register IPEN.

  1. RCONbits.IPEN = 1;

We deactiveren Brown-out Reset.

  1. RCONbits.SBOREN = 0; // BOR = off

When a timer is activated, it will increase the value of a register with 1 on fixed intervals. For timer1 this is a 16 bit register of which TMR1H represents the most significant byte and TMR1L the least significant byte. When this 16 bit register reaches its maximal value of 0xFFF an interrupt will be generated. By initializing the register in advance with a certain value, you can speedup the first occurrence of an interrupt. Hereto we initialize the register TMR1H with the 8 most significant bits and register TMR1L with the least significant bits of the variable timerServo. We use the binary AND operation and a shift operation to select the correct bits.

  1. TMR1H = (timerServo & 0xFF00) >> 8; // keep most significant byte
  2. TMR1L = timerServo & 0x00FF; // keep least significant byte

Next we set timer1 in such a way that ever 883.333 ns the *timer1 register is increased. This happens by setting the T1CON register in such a way that used instruction cycle TCYx (=83.333 ns) is delayed with a factor 8. In other words we use prescaler of 8. At the same time we activate timer1. In case you consult the PIC18F4550 datasheet External link in depth, you will find that we have to set the T1CON register to 0b00110001 in order to select a prescaler of 8 and activate timer1.

  1. T1CON = 0b00110001; // 48Mhz/4, prescaler 1/8

This also explains where the chosen value for the utmost positions of the servo MIN_TIMER_SERVO and MAX_TIMER_SERVO come from. For one of the utmost positions we want a waiting time of 0.7 ms. So we search a number to initialize the 16 bits register of timer1 so that after 0.7 ms its maximal value 65536 is reached. When we take the prescaler of 8 and the TCYx of 83.333 ns into account, we get the following equation: 0.7 ms = 83.3338(6553-x) ns with x the number we need. If we solve this simple equation we get that x should be 64536. In other words, we need to initialize the timer1 register to the value 64356 in order to get a pulse width of 0.7 ms. The value of 64356 is assigned to the variable timerServo. A similar logic can be applied for the other utmost position: 2.3 ms = 83.333ns8(65536-62086). After the pulse in the PWM signal comes a rest of 20 ms. Hereto, the timer1 register in the interrupt handler YourHighPriorityISRCode() is initialized to 35536 because 83.333ns8(65536-35536) is about 20 ms.

Successively we enable the Global Interrupt Enable bit (GIEH), activate the Overflow Interrupt Priority bit (TMR1IP) and the TMR1 Overflow Interrupt Enable bit (TMR1IE). All these actions are needed to use the timer1 interrupts. For more details we refer to the PIC18F4550 datasheet External link.

  1. INTCON = 0b10000000; // GIEH = TRUE
  2. IPR1bits.TMR1IP = 1;
  3. PIE1bits.TMR1IE = 1;

Ten slotte initialiseren we ook reeds de variabele leftSensor en rightSensor met een zinnige waarde.

  1. leftSensor = 512;
  2. rightSensor = 512;

Tracking light

After the initialization of all the needed points, we finally come to the logic of our program. This consists as always out of a loop that is executed an infinite amount of times, in which the following actions are performed:

  1. Store the old values of the two light sensors
  2. Read out the current sensor values
  3. Take the average of the current and old sensor values
  4. Compare the two sensor values and adjust the position
  5. Check is we have reached an utmost position
  6. Wait a little bit

If we convert this to code, we first make an infinite loop and execute step one.

  1. while(TRUE) { // do this forever
  2. // save old
  3. previousLeftSensor = leftSensor;
  4. previousRightSensor = rightSensor;

Next we read out the values of the sensors in step 2.

  1. // reading right light sensor
  2. SetChanADC(ADC_CH0); // choose ADC channel (override ADC_CH0)
  3. Delay10TCYx(150); // wait a bit
  4. ConvertADC(); // start conversion
  5. while(BusyADC()); // wait for conversion
  6. rightSensor = ReadADC(); // read result
  7.  
  8. // reading left light sensor
  9. SetChanADC(ADC_CH1); // choose ADC channel (override ADC_CH1)
  10. Delay10TCYx(150); // wait a bit
  11. ConvertADC(); // start conversion
  12. while(BusyADC()); // wait for conversion
  13. leftSensor = ReadADC(); // read result

In a third step we average out the sensor values. This is done because most sensors are susceptible to noise. Luckily this noise can be filtered out. The easiest way to do this is to take the average of the current and previous value.

  1. // filtering
  2. leftSensor = (leftSensor + previousLeftSensor)/2;
  3. rightSensor = (rightSensor + previousRightSensor)/2;

In the fourth step we let the program search from where the light comes. For this you have to take into account that how bigger the sensor value is, the less light there is. We can change the position by adding a value to or subtracting a value from the global variable timerServo. We do this with a fixed step that is set with the macro SERVO_STEP. We also use a threshold value to make sure that when both sensors receive about the same amount of light the servo keeps its current position. When later on your light tracker is finished, it is interesting to try different value for SERVO_STEP and THRESHOLD and evaluate their effect.

  1. if((rightSensor-leftSensor) > THRESHOLD)
  2. timerServo -= SERVO_STEP;
  3. else if((leftSensor-rightSensor) > THRESHOLD)
  4. timerServo += SERVO_STEP;

In the fifth step we check if we have reached one of the utmost positions. If this is the case we want to leave the value timerServo unchanged, because otherwise the servo might not be able to interpret the PWM signal any longer.

  1. if(timerServo > MAX_TIMER_SERVO)
  2. timerServo = MAX_TIMER_SERVO;
  3. if(timerServo < MIN_TIMER_SERVO)
  4. timerServo = MIN_TIMER_SERVO;

Finally we wait a little bit. You can increase or decrease this value to let your light tracker react slower or faster. Note that of course you have to wait for some time when reading out the analog sensors and that executing your code also takes some time. So your light tracker will never react at the speed of light.

  1. Delay10KTCYx(6); // wait 5 ms
  2. }
  3. }

Before you can test the code, you have to build a small construction. This is explained in the following two sections.

Connecting a servo

Typically a servo has three wires connected to connector. Two of these wires, the power lines, provide the servo with the correct voltage. The third one can be used to set the position of the servo. The two power lines are always colored black (convention for ground) and red (convention for 5 V). To easily connect servos, the Dwengo board is equipped with two connectors: servo1 and servo2. The connectors can be used to drive servo that require at most between 200 and 400 mA. Connect the servo in such a way that the dark brown or black wire corresponds with the "-"-sign as shown in the picture.

Connecting the servo


In this tutorial we assume that the servo is connected to the servo1 connector.

Connecting the servos

In the tutorial about ADC we showed how to build a light sensor with a resistor and a phototransistor. For the light tracker you have to build this circuit two times. However, in this case the idea is to solder the sensors in such a way that they can be mounted on the servo. Similarly as in the previous tutorial you connect one of the sensors to AN0 of the Dwengo board. The second sensor is connected to AN1 of the Dwengo board. Connect the long leg (+) of the phototransistor to the ground and the short leg (-) to the resistor that is connected with the 5 V pin. The connecting with wires is easily done on the Dwengo breadboard. You get a circuit as depicted in the photograph.

Circuit of the light tracker


When everything is well connected and programmed you have a working light tracker!

Overview of working light tracker

Source codeSource code files are only available to Dwengo customers. If you have bought with us before, please log in to download source files.