best way to read a servo signal on arduino

the atmega168 on the arduino has a timer1 which has input capture capability (see this application note and datasheet for atmega168 for details). using it, it's possible to do the pulse timing in completly in hardware (no errors, no jitter), and having a interupt readout the measured time.here's the code:unsigned int serinp[8]; //servo positionsvoid setup_timer1(){//disable all interuptsTIMSK1 &= ~( _BV(TOIE1) | _BV(ICIE1) | _BV(OCIE1A) | _BV(OCIE1B));//set timer modeTCCR1A &= ~( _BV(WGM11) | _BV(WGM10) );TCCR1B &= ~( _BV(WGM12) | _BV(WGM13) | _BV(ICNC1));//capture raising edgeTCCR1B |= _BV(ICES1); //capture raising edge//prescaler 1/8TCCR1B |= _BV(CS11);TCCR1B &= ~( _BV(CS12) | _BV(CS10) );//disable outputsTCCR1A &= ~( _BV(COM1A0) | _BV(COM1A1) | _BV(COM1B0) | _BV(COM1B1));//enable capture interuptTIMSK1 |= (1<<ICIE1);}ISR(TIMER1_CAPT_vect){static unsigned int lasticr; //icr at last caputrestatic unsigned char cserinp; //current input servounsigned int licr;//TCCR1B ^= _BV(ICES1);licr=ICR1-lasticr;lasticr=ICR1;if(licr>5000){ //pulse too long, means start of new framecserinp=0;}else if(licr>1000){ //pulse good, take reading, go to next channelserinp[cserinp]=licr;if(cserinp<8){cserinp++;}}else{//too short, nothing to see here}}void setup()pinMode( 8, INPUT ); //sumsetup_timer1();}void loop(){//use serinp!!!}it'll take a sum signal on pin 8. you can also connect the signal for a single servo, then just serinp[0] will be populated. the values in serinp are 2 times the microseconds, so between 2000-4000 (instead of 1000-2000), yes, there's extra accuracy in there!the timer-capture only works on digital pin 8!if you don't have access to the sum signal, a possible way to read multiple servos would be to connect them all to pin 8. since the receivers usually output the pulses one after the other, this should work (you'll have to adjust the code in the capture-interupt).
E-mail me when people leave their comments –

You need to be a member of diydrones to add comments!

Join diydrones

Comments

  • Thanks for the post and code.  Just joined to comment that I had a couple small issues getting it to compile on arduino.  Did a search on 1ICIE1 and at least one other person had trouble.

    --- these error --

    best_way_to_read_a_servo_signal_on_arduino.cpp:25:12: error: invalid suffix "ICIE1" on integer constant
    best_way_to_read_a_servo_signal_on_arduino.cpp:53:1: error: expected initializer before ‘pinMode’
    best_way_to_read_a_servo_signal_on_arduino.cpp:54:15: error: expected constructor, destructor, or type conversion before ‘;’ token
    best_way_to_read_a_servo_signal_on_arduino.cpp:55:1: error: expected declaration before ‘}’ token

    ---were corrected with this patch ----

    --- best_way_to_read_a_servo_signal_on_arduino.ino.orig 2012-12-05 06:55:05.934622586 -0500
    +++ best_way_to_read_a_servo_signal_on_arduino.ino 2012-12-05 06:56:45.926621361 -0500
    @@ -19,7 +19,7 @@
    TCCR1A &= ~( _BV(COM1A0) | _BV(COM1A1) | _BV(COM1B0) | _BV(COM1B1));

    //enable capture interupt
    -TIMSK1 |= (1ICIE1);
    +TIMSK1 |= _BV(ICIE1);
    }


    @@ -47,6 +47,7 @@


    void setup()
    +{
    pinMode( 8, INPUT ); //sum
    setup_timer1();
    }

    ---

    basically it was one typo ( or a lack of a macro on my part), and one missing opening brace.

    I'm just getting started so the errors could be on my end, but these changes worked for me.

    thanks again for the code

  • Hello Everyone!

    I have to measure the pulse width of very short pulses which I am giving to arduino from function generator with 1 Hz freq, the pulses are between 5 to 15 microseconds. I am using arduino nano board and here is my code which I am using but its not giving me good results so if someone can help me out here I would be grateful, here is my code

    #define ICP PINB0
    //Variables holding three timestamps
      volatile uint16_t ov_counter, rising, falling;
    //capture Flag
      volatile unsigned int flag = 0;
      volatile uint32_t counts;
      volatile uint16_t time;
      int val = 0;     // variable to store the read value
      int lastPulse = LOW;
      int count = 1;
    //Initialize timer
      float pulse_width= 0.0;

    //capture ISR
    ISR(TIMER1_CAPT_vect)
    {

    //if(ICP)
    //if (digitalRead(8) == HIGH)

    if ((PINB & B00000001) == B00000001)

                {

                //save start time

                rising=ICR1;

                //set to trigger on falling edge

                //TCCR1B&=~(1<<ICES1);
                 TCCR1B &= 0xBF;
                //reset overflow counter

                ov_counter=0;

       }

    else

                {

                //save falling time

                falling=ICR1;
                TCNT1 = 0;
                //rising edge triggers next
                
               //TCCR1B|=(1<<ICES1);
                TCCR1B |= 0x40;
                
                counts=(uint32_t)falling-(uint32_t)rising + (uint32_t)ov_counter;
            
                /*you can convert coutns to seconds and send to LCD*/
     
               
                 }
    }
    //Overflow ISR
    ISR(TIMER1_OVF_vect)
    {
    //increment overflow counter
    ov_counter++;
    }

    void setup()
    {
      Serial.begin(9600) ;
      delay(100);
      Serial.println("test");
    //Set Initial Timer value
    TCNT1=0;
    //First capture on rising edge
    TCCR1B|=(1<<ICES1)| (1<<CS10) | (1<<ICNC1);
    //TCCR1B|=(1<<ICES1)| (1<<CS10);
    //Enable input capture and overflow interrupts
    TIMSK1|=(1<<ICIE1)|(1<<TOIE1);
    //Enable global interrutps
    sei();
    }


    void loop()
    {

       val = digitalRead(8);   // read the input pin
                if (val != lastPulse)
       {
        
                   lastPulse = val;
                   
                  if (val == LOW)    
              {        
                
                 //   cli();
                    pulse_width = counts*4;
                    Serial.print("width = ");
                    Serial.print(pulse_width);
                    Serial.print("  count = ");
                    Serial.println(count++);
                                    //clear overflow counters;
                    ov_counter=0;

                    //clear interrupt flags to avoid any pending interrupts
                //    TIFR1=(1<<ICF1)|(1<<TOV1);
                    //enable input capture and overflow interrupts
                 //   TIMSK1|=(1<<ICIE1)|(1<<TOIE1);
               //   highCounter++;
                  
                //    sei();

                  }
             
       }


        
     
    }
     

  • Now I use the APM_radio code as suggested by Chris. I connect the PPM signal to the input pin 49 of the arduino Mega board and it works fine.

  • I want to read the 8 channel PPM Signal from an ACT Rx receiver using an arduinoMega. Can I try the code above?

  • Anyone have a link to a simple interrupt driven sketch that reads one channel and prints out the value?

  • I'm trying to avoid using pulsein on my code for my stepper based antenna tracker.   Here is the link to where I am at if you are curious... http://www.rcgroups.com/forums/showthread.php?t=1529524#post19715075

     

    Anyhow I am trying the code above with my atmega 328p pro mini and am unable to get a reading using pin 8 (always 0). 

    The code is below and I have tried another ISR(TIMER1_CAPT_vect) routine that I found on RCgroups but it doesn't work either.  Can anyone point me to some update code?  I just want to look at one channel from the RX and have it interrupt based. 

     

    thanks,

    Brendin

     


    #define enablePin 4



    // CODE FOR HARDWARE INTERRUPT

    unsigned int serinp[8]; //servo positions

    void setup_timer1(){
    //disable all interupts
    TIMSK1 &= ~( _BV(TOIE1) | _BV(ICIE1) | _BV(OCIE1A) | _BV(OCIE1B));

    //set timer mode
    TCCR1A &= ~( _BV(WGM11) | _BV(WGM10) );
    TCCR1B &= ~( _BV(WGM12) | _BV(WGM13) | _BV(ICNC1));

    //capture raising edge
    TCCR1B |= _BV(ICES1); //capture raising edge

    //prescaler 1/8
    TCCR1B |= _BV(CS11);
    TCCR1B &= ~( _BV(CS12) | _BV(CS10) );

    //disable outputs
    TCCR1A &= ~( _BV(COM1A0) | _BV(COM1A1) | _BV(COM1B0) | _BV(COM1B1));

    //enable capture interupt
    TIMSK1 |= (1<<ICIE1);
    }



    /*
    ISR(TIMER1_CAPT_vect){
    static unsigned int lasticr; //icr at last caputre
    static unsigned char cserinp; //current input servo

    unsigned int licr;

    //TCCR1B ^= _BV(ICES1);
    licr=ICR1-lasticr;
    lasticr=ICR1;

    if(licr>5000){ //pulse too long, means start of new frame
    cserinp=0;
    }else if(licr>1000){ //pulse good, take reading, go to next channel
    serinp[cserinp]=licr;
    if(cserinp<8){
    cserinp++;
    }
    }else{
    //too short, nothing to see here
    }
    }
    */

    ISR(TIMER1_CAPT_vect){
    static unsigned int lasticr; //icr at last caputre
    static unsigned char cserinp; //current input servo

    unsigned int licr;

    //TCCR1B ^= _BV(ICES1);
    licr=ICR1-lasticr;
    lasticr=ICR1;

    if (licr<10000) { //pulse not super long - so Ok, otherwise totally ignore


    if(licr>5000){ //pulse too long, means start of new frame
    cserinp=0;
    serinp[cserinp]=licr;
    cserinp++;
    }else if(licr>500){ //pulse good, take reading, go to next channel
    serinp[cserinp]=licr;
    if(cserinp<8){
    cserinp++;
    }
    }else{
    //too short, nothing to see here
    }
    //updateTXVals();  // So at the end lets update our calibrate vals 
    }

    }

    // END HARDWARE INTERRUPT CODE


    void setup()

     
      Serial.begin(9600);
    Serial.println("Setting up timer.");

      pinMode( 8, INPUT ); //sum
     setup_timer1();
     
    Serial.println("done setting up timer."); 



    }

    void loop()
    {

     channel=serinp[0];

    if ((serinp[0] >0) or (serinp[0] <0) ){
     Serial.print("Value = " );
     Serial.println(serinp[0]);
    }
     
    }

     

  • Chris!!! mi salvador!! hehehe

    Where can i find that pwm code??

    10x!
  • 3D Robotics
    Aldo,

    I'd use the PWM code from ArduPilot, which has advanced this technique quite a bit.
  • Hi!

    I was trying to use your code, but i have some problems or mistakes...

    Im reading the ppm channel, only for the first 2 channels... and got something like this:

    1419 1506
    1419 1506
    1418 1503
    1418 1503
    1421 1503
    1420 1502
    1420 1502
    1422 1502
    1422 1502
    1424 1502
    1424 1501
    1426 1501

    Then, i try to send that value to the servo...

    And the servo goes very high! i a position that doesnt correspond to +-1500 values, what can be wrong??

    This is my code:

    #include
    #define ser1out 2//Servo Exterior
    #define ser2out 3//Servo Interior
    unsigned int serinp[2]; //servo positions

    ServoTimer2 ciLat;
    ServoTimer2 ciLon;

    void setup(){
    Serial.begin(9600);
    pinMode( 8, INPUT ); //sum
    ciLat.attach(ser1out);
    ciLon.attach(ser2out);
    setup_timer1();
    }

    void loop()
    {
    Serial.print(serinp[0]/2);
    Serial.print(" ");
    Serial.println(serinp[1]/2);
    ciLat.write(serinp[0]/2);
    ciLon.write(1500);
    }

    The rest is your functions...

    With one servo i write to it 1500, and it works fine, but the othe servo, is just crazy... what im doing wrong??

    Thanks!!

    Cheers!!
  • Hi!

    Nice code, quix-fz, many thanks! I tried in on my Arduino and: it works! My setup is:

    Spektrum DX6i + AR6100e (plus PWM to PPM Converter from rc-cam.com)
    Arduino Nano 3.0 (ATMega 328)
    LCD Display (it is very convinient to see all channel values)
    ADXL330 (I want to implement a simple wing leveler for my Easy Glider)

    I had to use ServoTimer2 library for servo control since Servo uses Timer1. LiquidCrystal had to be modified as well to avoid using delay() functions.
This reply was deleted.