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 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 |= (1ICIE1);
}


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
}
}


void setup()
pinMode( 8, INPUT ); //sum
setup_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).

Views: 9149

Comment by Aaron Bonnell-Kangas on July 5, 2008 at 7:25pm
Okay. This is quite a bit over my head-- I really haven't gotten this far into Arduino yet, and this just makes my head spin. I understand that when you do this particular setup and connect a pulse train to pin 8, the serinp array will be populated with values corresponding to PWM values coming in on the train?

What do the numbers in serinp correspond to? Milliseconds? Clock cycles? Something else?

When I get back to my Arduino and RX, I'll test this out for myself and hopefully get some more understanding of this code. Sorry for such dumb questions-- if there's some resource I can use to learn this on my own, I'd be happy to do that. I understand you don't have all the time in the world. :]

Thanks for any and all help.
Comment by Aaron Bonnell-Kangas on July 5, 2008 at 7:39pm
Bink. And kaldak on RCGroups points out that the answer to my question is right in front of me. Sorry about that. :]
Comment by quix-fz on July 10, 2008 at 12:16am
hey! thanks for the interest :)
here's some understandable descrition of interupts:
http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/

the code first setups the timer1. this is the only 16bit timer on the atmega168/arduino. this means it counts from 0 to 65536 and then again from zero. the number is increased every 1/8 clock (arduino is 16mhz, so the number is increased every 8/16000000 = 1/2000000 second!!). the input capture is also setup, it "records" the time (the time (16bit counter) from timer1) when the signal goes high on the input capture pin (thats digital 8 on the arduino) to the internal register ICR1. this is completely done in hardware, no matter what else is going on!!! even if your software is crashed this will probably continue to work (including the following part). when signal goes high, an interupt (basicly a function) is called too, which reads ICR1 and subtracts the old ICR1 the get the pulse-length and decodes the ppm.
Comment by MercuryTree on June 4, 2009 at 8:53pm
Hi,

Thanks for the ICR PPM capture code. I am attempting to use it but have hit a snag. I am also using the analogWrite() procedure to drive ESC's @ 488Hz and apparently timer1 which your code uses is also responsible for pins 9,10 which I need for the PWM drivers. Is there anyway that you can think of to modify your code so as to still allow pins 9 and 10 to be used to driver hardware PWM or should I just ditch the idea?

Thanks!
Comment by Leonid Scharf on September 11, 2009 at 7:48am
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.
Comment by Aldo Vargas on March 2, 2010 at 10:44am
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!!

3D Robotics
Comment by Chris Anderson on March 2, 2010 at 10:47am
Aldo,

I'd use the PWM code from ArduPilot, which has advanced this technique quite a bit.
Comment by Aldo Vargas on March 2, 2010 at 10:49am
Chris!!! mi salvador!! hehehe

Where can i find that pwm code??

10x!
Comment by Brendin on October 30, 2011 at 10:29am

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 |= (1ICIE1);
}



/*
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]);
}
 
}

 

Comment by Brendin on October 30, 2011 at 3:48pm

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

Comment

You need to be a member of DIY Drones to add comments!

Join DIY Drones

© 2019   Created by Chris Anderson.   Powered by

Badges  |  Report an Issue  |  Terms of Service