RC receiver signal jitter with teensy 2.0

Hello,

I'm using a teensy 2.0 board( amtel MEGA32U4) to receive 4 channels signals from a 40Mhz futaba RC receiver. I've tried pulsein and interrupt methods which are working great but in both cases, my values are jittering.

I've read many subjects on this forum but did not find the clue. For the moment I've put an averaging algorythm in place but it slow down a lot my program as I need several valid inputs to average. And with that it still jittering !

Any help would be really appreciated

Here is an example of the input values that I get on Channel 1 (other are jittering the same and are always multiple of 4) :

  rc1_val raw:  1540
  rc1_val raw:  1540
  rc1_val raw:  1540
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1512
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1536
  rc1_val raw:  1520
  rc1_val raw:  1520
  rc1_val raw:  1520
  rc1_val raw:  1520
  rc1_val raw:  1520

Here is the code I use with the 4 interrupt pins (found on this forum) :

//read PPM signals from 2 channels of an RC reciever and convert the values to PWM in either direction.
// digital pins 5 & 9 control motor1, digital pins 6 & 10 control motor2. DP 12 and 13 are neutral indicator lights. DP 2 and 3 are inputs from the R/C receiver. All analog pins are open. When motor pin is HIGH, bridge is open. All mosfets are pulled up/down to stay closed unless told to open.
unsigned long time; // variable de mémorisation du temps écoulé
 unsigned long timeold ;// au cycle précédent; // variable de mémorisation du temps écoulé
 float timediff; //temps du cycle écoulé

int motor1_a = 7;
int motor1_b = 8;

int motor2_a = 9;
int motor2_b = 10;

//Neutral indicator LED's (FWD and REV LED's are on the motor driver board.
int ledPin1 = 12;
int ledPin2 = 13;

int ppm1 = 5;  // connect the desired channel (PPM signal) from your RC receiver to digital pin 2 on Arduino.
int ppm2 = 6;
int ppm3 = 7;  // connect the desired channel (PPM signal) from your RC receiver to digital pin 2 on Arduino.
int ppm4 = 8;

unsigned long rc1_PulseStartTicks;
volatile int rc1_val;  // store RC signal pulse length
int lastgood1;
int adj_val1;  // mapped value to be between 0-511
int rc1_InputReady;

unsigned long rc2_PulseStartTicks;
volatile int rc2_val;  // store RC signal pulse length
int lastgood2;
int adj_val2;  // mapped value to be between 0-511
int rc2_InputReady;

unsigned long rc3_PulseStartTicks;
volatile int rc3_val;  // store RC signal pulse length
int lastgood3;
int adj_val3;  // mapped value to be between 0-511
int rc3_InputReady;

unsigned long rc4_PulseStartTicks;
volatile int rc4_val;  // store RC signal pulse length
int lastgood4;
int adj_val4;  // mapped value to be between 0-511
int rc4_InputReady;

int deadband_high = 265; // set the desired deadband ceiling (ie. if adj_val is above this, you go forward)
int deadband_low = 245;  // set deadband floor (ie. below this, go backward)

int pwm_ceiling = 256; // adjusts speed of PWM signal at turn-on
int pwm_floor = 254;

int delta_val = 400; //this value sets the range for discarding unwanted signals.

void setup() {

  Serial.begin(9600);


  //led's
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);

  //PPM inputs from RC receiver
  pinMode(ppm1, INPUT); //Pin 2 as input
  pinMode(ppm2, INPUT); //Pin 3 as input
  pinMode(ppm3, INPUT); //Pin 2 as input
  pinMode(ppm4, INPUT); //Pin 3 as input
  attachInterrupt(0, rc1, CHANGE);    // catch interrupt 0 (digital pin 2) going HIGH and send to rc1()
  attachInterrupt(1, rc2, CHANGE);    // catch interrupt 1 (digital pin 3) going HIGH and send to rc2()
attachInterrupt(2, rc3, CHANGE);    // catch interrupt 0 (digital pin 2) going HIGH and send to rc1()
  attachInterrupt(3, rc4, CHANGE);    // catch interrupt 1 (digital pin 3) going HIGH and send to rc2()

  lastgood1 = 255;
  lastgood2 = 255;
  lastgood3 = 255;
  lastgood4 = 255;

}


void rc1()
{
  // did the pin change to high or low?
  if (digitalRead( ppm1 ) == HIGH)
  {
    // store the current micros() value
    rc1_PulseStartTicks = micros();
  }
  else
  {
    // Pin transitioned low, calculate the duration of the pulse
    rc1_val = micros() - rc1_PulseStartTicks; // may glitch during timer wrap-around
    // Set flag for main loop to process the pulse
    rc1_InputReady = true;
  }
}

void rc2()
{
  // did the pin change to high or low?
  if (digitalRead( ppm2 ) == HIGH)
  {
    // store the current micros() value
    rc2_PulseStartTicks = micros();
  }
  else
  {
    // Pin transitioned low, calculate the duration of the pulse
    rc2_val = micros() - rc2_PulseStartTicks; // may glitch during timer wrap-around
    // Set flag for main loop to process the pulse
    rc2_InputReady = true;
  }
}
void rc3()
{
  // did the pin change to high or low?
  if (digitalRead( ppm3 ) == HIGH)
  {
    // store the current micros() value
    rc3_PulseStartTicks = micros();
  }
  else
  {
    // Pin transitioned low, calculate the duration of the pulse
    rc3_val = micros() - rc3_PulseStartTicks; // may glitch during timer wrap-around
    // Set flag for main loop to process the pulse
    rc3_InputReady = true;
  }
}

void rc4()
{
  // did the pin change to high or low?
  if (digitalRead( ppm4 ) == HIGH)
  {
    // store the current micros() value
    rc4_PulseStartTicks = micros();
  }
  else
  {
    // Pin transitioned low, calculate the duration of the pulse
    rc4_val = micros() - rc4_PulseStartTicks; // may glitch during timer wrap-around
    // Set flag for main loop to process the pulse
    rc4_InputReady = true;
  }
}

void loop() {

//////////////////////////////////signal1
    if (rc1_InputReady)
  {
    // reset input flag
    rc1_InputReady = false;

    // constrain and map the pulse length
    adj_val1 = map(constrain(rc1_val, 1000, 2000), 1000, 2000, 0, 511);

    if (adj_val1 == 0){       // if value is 0, use last good value
    adj_val1 = lastgood1;
    }
    else if (adj_val1 == 511){   // if value is 511, use last good value
    adj_val1 = lastgood1;
    }
    else if (((adj_val1 - lastgood1) * 2) > delta_val){   // make sure the new value is within a reasonable range of the last known good value.
    adj_val1 = lastgood1;
    }
    else {
    lastgood1 = adj_val1;   // if all conditions are met, use new value and set it as lastgood1 value.
    }

}

//////////////////////////////////signal2
    if (rc2_InputReady)
  {
    // reset input flag
    rc2_InputReady = false;

    // constrain and map the pulse length
    adj_val2 = map(constrain(rc2_val, 1000, 2000), 1000, 2000, 0, 511);

    if (adj_val2 == 0){       // if value is 0, use last good value
    adj_val2 = lastgood2;
    }
    else if (adj_val2 == 511){   // if value is 511, use last good value
    adj_val2 = lastgood2;
    }
    else if (((adj_val2 - lastgood2) * 2) > delta_val){   // make sure the new value is within a reasonable range of the last known good value.
    adj_val2 = lastgood2;
    }
    else {
    lastgood2 = adj_val2;   // if all conditions are met, use new value and set it as lastgood1 value.
    }

}


//////////////////////////////////signal3
 //removed due to message size on forum

//////////////////////////////////signal4
  
 //removed due to message size on forum


 // Mesure du temps de cycle
  time = micros(); 
  timediff = ((time - timeold)) ; // calcul différence de temps et conversion en fréquence
  timeold = time;
//  Serial.print("  Temps cycle :  ");
//  Serial.println(timediff);
  
  
  //print values
  Serial.print("channel 1:  ");
  Serial.print(adj_val1);
  Serial.print("     ");
  Serial.print("rc1_val raw:  ");
  Serial.print(rc1_val);
  Serial.print("     ");
  Serial.print("channel 2:  ");
  Serial.print(adj_val2);
  Serial.print("     ");
  Serial.print("rc2_val raw:  ");
  Serial.print(rc2_val);
  Serial.print("     ");
   Serial.print("channel 3:  ");
  Serial.print(adj_val3);
  Serial.print("     ");
  Serial.print("rc3_val raw:  ");
  Serial.print(rc3_val);
  Serial.print("     ");
  Serial.print("channel 4:  ");
  Serial.print(adj_val4);
  Serial.print("     ");
  Serial.print("rc4_val raw:  ");
  Serial.print(rc4_val);
  Serial.println("   ");
}

The very first thing to do in the interrupt routines is call micros() - that way the code in the rest of the interrupt routine can't add jitter to the timings.

Thanks for your feedback XD, seems very logic so I've implemented that

void rc1()
{
  
  rc1_micros = micros();// fill the value at beginning of interrupt (replace micros by RC1_micros)
// did the pin change to high or low?

  if (digitalRead( ppm1 ) == HIGH)
  {
    // store the current micros() value
    rc1_PulseStartTicks = rc1_micros;
  }
  else
  {
    // Pin transitioned low, calculate the duration of the pulse
    rc1_val = rc1_micros - rc1_PulseStartTicks; // may glitch during timer wrap-around
    // Set flag for main loop to process the pulse
    rc1_InputReady = true;
  }
}

Unfortunatly the results of input measure after this modification are quite the same than before, jitter between 1512 and 1540 which is not precise enough to avoid servo jittering...

Is there any other way to enhance this or any other way to explorate ?

It might be that the timing is affected by the some other interrupt handler on the 32U4? The datasheet will say what the interrupt priorities are. Other than that I’m out of ideas. Perhaps the implementation of micros() is flawed for some other reason?

Thanks for your help Markt!!!!

At this step I cannot find what's wrong, my program is not using the other interrupts... =( =(

Can someone confirm me that with arduino products, we are able to decode an RC signal and have stable values( (can accept a +4 -4 precision but not +20 -20)) or is it the normal behaviour that the values measured are moving arround the signal value ?

Any help would be greatly appreciated as I'm stopped by this issue and I'm spending all my evenings on the net to find a solution.

The best way to measure a relatively low frequency is with the dedicated hardware timer input capture feature. I wrote a library that does this.

http://www.pjrc.com/teensy/td_libs_FreqMeasure.html

There's a similar library for higher frequencies. Martin Nawrath also wrote 2 very similar libraries, long before I did, and they are linked at the bottom of that page. But mine fixes a number of minor issues and offers compatibility with more hardware (including Teensy), so I'd recommend you use mine. :-)

If you really want to do this with attachInterrupt(), the bad news is any change in interrupt response latency directly introduces jitter, as you're seeing.

MarkT pointed out interrupts, and indeed the Timer0 interrupt is always enabled. Approximately every 1ms, it interrupts to run a bit of code that updates the variables used by delay(), millis(), micros(), etc.

Interrupt are also disabled by certain functions you might be using. For example, on Teensy, Serial.print() writes to a USB virtual serial port, rather than a real serial port which talks to your computer via a USB-serial converter chip. That's a lot faster, both due to USB's incredible speed, and hardware buffering. But Teensy's Serial.print() disables interrupts while copying your text into the USB packet buffers.

If the signal edge happens when interrupts are off, either due to another interrupt or normal code temporarily disabling interrupts, your interrupt handler won't run until they are enabled again.

So you could try to work this, but micros() won't work properly for more than a very short time if you shut off the Timer0's interrupt. You could try some very difficult things, like reading timer0 before and after and trying to guess if the interrupt happened and messed up your measurement. Of course, you'd anticipate when your next interrupt might happen and try to schedule any interrupt-disabling activity for times when you're not likely to get an interrupt. But that's a LOT of work, and probably prone to being unreliable at best.

The FreqMeasure library is definitely the way to go!

ps: the multiples of 4 is because micros() is using Timer0, which is configured for 1/16 of the CPU clock... so at 16 MHz you get 4 us resolution. The FreqMeasure library uses one of the other more capable timers running at the full CPU speed, so you'll get 250 ns resolution at 16 MHz.

Thanks a lot Paul for your feedback.

At this step I did not achieve to decode the Receiver’s signal using the libray as the frequency is not the exact thing that I’m trying to read (but the pulse length of 1 event).
But the very interesting thing for me is to understand better what I can expect in term of precision using the interrupt or other methods and for that your answer wasd incredibly interresting.

Of course if someone has complementary infos on how to decode a RC signal using the library mentionned by Paul, I’m very interested in !

I believe you should still give the library a try.

The FreqMeasure library measures precise pulse length. If you only want to measure 1 pulse, you can do that. Just call available() and read() to discard everything measured before your pulse begins, and then available() / read() to read only your 1 pulse. After you've got the result you need, just don't bother calling the functions anymore. When you do need to make another measurement, just be sure to call them to discard any measurements it made that you don't want.

FreqMeasure will give you vastly superior results.