Not sure if this is the correct category; moderator please move if necessary
I am trying to develop an application in which my Uno Rev3 reads PWM pulses from an RC Receiver and then drives external servos. As an inexperienced programmer, I am writing small pieces of code to test out functions I will need.
After some initial confusion about input pin numbering, I have sketches which successfully read the PWM inputs from two Receiver channels using the pulesIn command. I am displaying the readings in two 4-digit columns on my monitor. After seeing some variation ~20usec on these readings I added code to maintain a running average of five on each channel. I then see the reported readings become very static never changing by more than 1usec (unless of course I move the Transmitter's sticks).
Next part is to use these averaged input readings to drive servos, with the same values just for now, as a test. I attach the servo library, declare the servo outputs etc. After uploading the sketch I can control the servos from my RX Transmitter's sticks but they have some jitter. Looking at the monitor I can see the pulseIn readings suddenly have more variation again, as bad as the earlier results without averaging.
Playing around with commenting out various of the servo commands I discovered that the variations in reported input appear as soon as I include the "servo.attach(pin#)" commands in the sketch, even with the later "writeMicrosecond" commands to the servos commented out.
I am powering the servos from a separate 6V battery, common GND with Uno, signal wires connecting to Arduino digital pins. I wondered if perhaps the servos were affecting the Arduino in some way, perhaps pulling its outputs towards +6V, but even with the servos unplugged and no +6V present the pulseIn measurement variations appear as soon as the servo.attach commands are included in the compilation.
Does anyone have any suggestions? It's looking more and more like I need to find a scope if I am to carry on with this stuff.
I know that Pulsein is blocking. Looking at the source code of PulseIn I see it's hanging in a while loop until the pulse ends or it exceeds the timeout. The Servo library uses interrupts, which get a higher priority than the "hanging" Pulsein. So that may cause your deviations in measuring the pulse-length.
copied from "wiring_pulse.c"
/* Measures the length (in microseconds) of a pulse on the pin; state is HIGH
* or LOW, the type of pulse to measure. Works on pulses from 2-3 microseconds
* to 3 minutes in length, but must be called at least a few dozen microseconds
* before the start of the pulse.
*
* ATTENTION:
* this function relies on micros() so cannot be used in noInterrupt() context
*/
unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout)
{
// cache the port and bit of the pin in order to speed up the
// pulse width measuring loop and achieve finer resolution. calling
// digitalRead() instead yields much coarser resolution.
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
uint8_t stateMask = (state ? bit : 0);
unsigned long startMicros = micros();
// wait for any previous pulse to end
while ((*portInputRegister(port) & bit) == stateMask) {
if (micros() - startMicros > timeout)
return 0;
}
// wait for the pulse to start
while ((*portInputRegister(port) & bit) != stateMask) {
if (micros() - startMicros > timeout)
return 0;
}
unsigned long start = micros();
// wait for the pulse to stop
while ((*portInputRegister(port) & bit) == stateMask) {
if (micros() - startMicros > timeout)
return 0;
}
return micros() - start;
}
In my opinion it works better to capture the PWM signals from an RC receiver in a non-blocking way.
This jitter in the timing is indeed due to interrupts. This will happen even without the servo library calls too, since there are interrupts used to maintain the record time for millis() and micros().
This is an unavoidable issue with using software polling for determining timing of an external signal. This can be mitigated by using a faster processor, but not removed.
Only hardware timing can avoid this jitter, and on the Uno there's only one pin I believe that can do this (pin 11 is routed to timer1). By suitably programming timer1 it can record the counter values when that input changes, and thus record pulse durations with resolution of upto 62.5ns (for 16MHz processor).
However if you want to use servos as well you'll need a library for servos that uses a different timer than timer1 - however this is problematic as on the Uno this is the only 16-bit timer.
Thank you all for the interesting replies. My first sketch executed pulseIn commands for receiver channel #1 then channel #2 and I noticed that reported readings for #2 did not work properly over the full range of values for #1. I do not know how much, if any, guard time there is between RC Rcvr output pulses on the different channels so I suspected I was running into problems with #2 occurring too soon after #1. The above info about pulseIn needing several dozen microseconds before the leading edge reinforces this. I had cured this problem by adding a 1 msec delay after the pulseIn #1 command.
Regarding servo library interrupts, I can see these occurring while I am executing writeMicroseconds commands which must use a timer, but as I said the jitter appears even if I comment out those commands and indeed all servo-related commands other then the two servo.attach'es. Are there ongoing interrupts simply from having these?
Lastly it had occurred to me that I could switch the input PWM from the RC Rcvr to pins 2 and 3 and use interrupts and the micros() function to "manually" measure the pulse widths, but I kept trying to get pulseIn to work because I don't know what unforeseen side effects doing this may incur. I will move the wires anyway to try this option.
Finally, here is the code for those interested. I pretty much copied and example sketch to do the averaging. The "select" variable just toggles back and forth to ensure that I read only one of the two channels each 20msec RC cycle; idea being that I have no problems with trying to read #2 too soon after #1. After reading whichever I "writeMicroseconds" to the output servos using the accumulated averages, thus the actual servos see the proper 50Hz rate pulses.
Again, the jitter appears the moment I "uncomment" the servo.attach commands even with subsequent servo commands commented out.
ps: where is this "<>" button? I don't see it.....
int throttle;
int rudder;
int select = 0;
const int numReadings = 5;
int throttleReadings[numReadings]; //array to store readings for throttle
int rudderReadings[numReadings]; //array to store readings for rudder
int readIndexThrottle = 0;
int readIndexRudder = 0;
int throttleTotal = 0;
int rudderTotal = 0;
int throttleAverage = 0;
int rudderAverage = 0;
#include <Servo.h>;
Servo FirstServo;
Servo SecondServo;
void setup() {
Serial.begin(9600);
pinMode(10, INPUT); // from Rcvr Ch1
pinMode(9, INPUT); // from Rcvr Ch2
//FirstServo.attach(16);
//SecondServo.attach(17);
// initialize all throttle and rudder readings to zero;
for (int thisreading = 0; thisreading < numReadings; thisreading++) {
throttleReadings[thisreading] = 0;
rudderReadings[thisreading] = 0;
}
}
void loop() {
if (select == 0) {
// subtract the last reading:
throttleTotal = throttleTotal - throttleReadings[readIndexThrottle];
// measure a new pulse:
throttleReadings[readIndexThrottle] = pulseIn(10, HIGH, 25000);
// add the reading to the total:
throttleTotal = throttleTotal + throttleReadings[readIndexThrottle];
//advance to the next position in the array:
readIndexThrottle = readIndexThrottle + 1;
// if we're at the end of the aray....
if (readIndexThrottle >= numReadings) {
// wrap around to the beginning:
readIndexThrottle = 0;
}
// calculate the average:
throttleAverage = throttleTotal / numReadings;
// toggle the select value
select = 1 - select;
Serial.print(throttleAverage);
Serial.print(" ");
delay(1);
}
else if (select == 1) {
// subtract the last reading:
rudderTotal = rudderTotal - rudderReadings[readIndexRudder];
// measure a new pulse:
rudderReadings[readIndexRudder] = pulseIn(9, HIGH, 25000);
// add the reading to the total:
rudderTotal = rudderTotal + rudderReadings[readIndexRudder];
//advance to the next position in the array:
readIndexRudder = readIndexRudder + 1;
// if we're at the end of the aray....
if (readIndexRudder >= numReadings) {
// wrap around to the beginning:
readIndexRudder = 0;
}
// calculate the average:
rudderAverage = rudderTotal / numReadings;
// toggle the select value
select = 1 - select;
Serial.println(rudderAverage);
delay(4);
}
//FirstServo.writeMicroseconds(throttleAverage);
//SecondServo.writeMicroseconds(rudderAverage);
}
// This program is derived by adding servo-specific stuff to the program
// "Input_Pulse_2channel_averaged" which delivered rock steady
// averages for throttle and rudder input pulse width (providing the
// delays were set properly). But the moment the servo.attach
// commands are included in the compilation, the averages become very
// unsteady and fluctuate as if no averaging was occurring (even
// with the writeMicroseconds commands commented out). This extra
// variation occurs even with no servos connected and no +6V.
// Don't know why, don't know how. May need an oscilloscope.
If you can put your code in codetags, so I can copy it to my IDE, I can have a look at my old codes, if I can find something similar. I have used another library for servo input and output in the past that used all pin change interrupts (I think), and gave me less jitter.
Your RC receiver does not have a (C)PPM output by accident (all channels on one pin)?
Once you attach a pin to the Servo object the Servo library starts sending 20 pulses per second per servo. The '.write() and .writeMicroseconds() functions just change the length of the pulse.
You can get very precise time measurements, independent of interrupts, with the Input Capture Register feature. Unfortunately, there is only one Input Capture pin on the UNO/Nano and only 2 on the Mega.
The pulseIn() function is counting instruction cycles but that stops for the duration of each interrupt. Another choice is to use external or Pin Change interrupts to grab the value of micros() which has a 4-microsecond resolution. That would only get a bad reading if a different interrupt happened to trigger a few instruction cycles before yours.
If you want really good pulse numbers, set up your own high-speed timer on Timer2 and sample that with interrupts. That will give you 64 times the timer resolution.
I tried your code, and that did not work for me. I got weird PWM values and when I deleted the delays in the code the pwm value got better but I only got values for the first channel and not for the second. I could confirm the servo jitter though.
So then I dug something up I created in the past and simplified it to be closer to your sketch. Throttle/Rudder in and Throttle/Rudder out.
I have been using the DigisparkArduinoIntegration libraries for many years for a lot of things that have to do with interpreting signals from RC receivers and moving servo's. It gives me less servo jitter, though not 100% perfect. For most applications it performs just fine.
Assuming you know how to install these libraries first, you can try below sketch and see the result for yourself.
There is >30 libraries in the package that I am referring to, but I am only including 3 of them
/*
Test sketch to accept 2 RC channels and output to 2 servo's
Based on SoftRClibs
https://forum.arduino.cc/t/attaching-a-servo-affects-pulsein-readings/911476/11
Oct 2021 by Hans Meijdam
*/
//#define DEBUG // uncomment to activate debugging via serial monitor
//Non-Blocking servo library SoftRc
#include <SoftRcPulseIn.h>
#include <SoftRcPulseOut.h>
#include <TinyPinChange.h> /* Needed for <SoftRcPulseIn> library */
//declaration of pins
#define Rudder_CHANNEL_PIN 9 // input
#define Throttle_CHANNEL_PIN 10 // input
#define Rudder_SERVO_PIN 11 // output
#define Throttle_SERVO_PIN 12 // output
//declaration of static variables
#define PULSE_MAX_PERIOD_MS 25 /* To refresh the servo in case of pulse extinction */
#define PULSE_MIN_ALIVE_MS 500 /* To refresh the servo in case of pulse extinction */
#define NOW 1
#define NEUTRAL_US 1500 /* Default position in case of no pulse at startup */
enum {Rudder_SERVO=0, Throttle_SERVO, SERVO_NB}; /* Trick: use an enumeration to declare the index of the servos AND the amount of servos */
//Create the SoftRCpulseInOut library objects
SoftRcPulseIn RudderChannelPulse; /* is an object of SoftRcPulseIn type */
SoftRcPulseIn ThrottleChannelPulse; /* is an object of SoftRcPulseIn type */
SoftRcPulseOut ServoMotor[SERVO_NB]; /* Table Creation for objects of SoftRcPulseOut type */
/* Variables that will change*/
uint32_t RxPulseStartMs=millis();
uint32_t RxPulseAliveMs=millis();
int rudder_pwm;
int throttle_pwm;
//Smoothing RcPulseIn channels by rolling average with (numReadings) samples
const unsigned char numReadings = 10; // 10 samples (if more smoothing required, increase number)
unsigned int rudder_readings[numReadings]; // Array to store the rudder readings
unsigned int throttle_readings[numReadings]; // Array to store the throttle readings
unsigned char index = 0; // the index of the current reading
unsigned long rudder_total = 0; // the running rudder total
unsigned long throttle_total = 0; // the running throttle total
unsigned int rudder_average = 0; // the rudder average
unsigned int throttle_average = 0; // the throttle average
void setup()
{
RudderChannelPulse.attach(Rudder_CHANNEL_PIN);
ThrottleChannelPulse.attach(Throttle_CHANNEL_PIN);
ServoMotor[Rudder_SERVO].attach(Rudder_SERVO_PIN); // Attach pin to servo object
ServoMotor[Throttle_SERVO].attach(Throttle_SERVO_PIN); // Attach pin to servo object
#ifdef DEBUG
Serial.begin(115200);
#endif
}
void loop()
{
static uint16_t Width_us=NEUTRAL_US; /* Static to keep the value at the next loop */
/* Receiver pulse acquisition and command of output channels */
if(RudderChannelPulse.available())
{
rudder_pwm = RudderChannelPulse.width_us();
throttle_pwm = ThrottleChannelPulse.width_us();
//input channel smoothing
// rudder_pwm = constrain(rudder_pwm, 800, 2200); // effectively ensuring that it is in range)
// throttle_pwm = constrain(throttle_pwm, 800, 2200); // effectively ensuring that it is in range)
rudder_total= rudder_total - rudder_readings[index]; // subtract the rudder last reading
throttle_total= throttle_total - throttle_readings[index]; // subtract the throttle last reading
rudder_readings[index] = rudder_pwm; // read new rudder value into smooting array
throttle_readings[index] = throttle_pwm; // read new throttle value into smooting array
rudder_total= rudder_total + rudder_readings[index]; // add the new value to the total:
throttle_total= throttle_total + throttle_readings[index]; // add the new value to the total:
index = index + 1; // advance to the next position in the array:
if (index >= numReadings) // if we're at the end of the array...
index = 0; // ...wrap around to the beginning:
rudder_average = rudder_total / numReadings; // calculate the rudder average:
throttle_average = throttle_total / numReadings; // calculate the throttle average:
#ifdef DEBUG
Serial.print("rudder: ");
Serial.print(rudder_average);
Serial.print(" throttle: ");
Serial.println(throttle_average);
#endif
ServoMotor[Rudder_SERVO].write_us(rudder_average); // Direct Signal
ServoMotor[Throttle_SERVO].write_us(throttle_average); // Direct Signal
SoftRcPulseOut::refresh(NOW); /* NOW argument (=1) allows to synchronize outgoing pulses with incoming pulses */
RxPulseStartMs=millis(); /* Restart the Chrono for Pulse */
RxPulseAliveMs=millis(); /* Restart the Chrono for Alive */
}
else
{
if(millis()-RxPulseAliveMs>=PULSE_MIN_ALIVE_MS){
ServoMotor[Throttle_SERVO].write_us(800); // Safety cutoff if no RC pulse
ServoMotor[Rudder_SERVO].write_us(1500); // Rudder to neutral if no RC signal
}
/* Check for pulse extinction */
if(millis()-RxPulseStartMs>=PULSE_MAX_PERIOD_MS)
{
/* Refresh the servos with the last known position in order to avoid "flabby" servos */
SoftRcPulseOut::refresh(NOW); /* Immediate refresh of outgoing pulses */
RxPulseStartMs=millis(); /* Restart the Chrono for Pulse */
}
}
//Put rest of code here, the fewer the better, avoid delays
// delay(10); //for testing routine slowdown effect
}
Yes, I do not understand why I need the delays either. I also saw one channel return zero without the 2nd delay.
Thank you for this code! I don't pretend to understand it all right now and I've been busy working this week so haven't done much. However, I finally decided to order an oscilloscope (Rigol Canada has a discount on two of their entry-level models for the fall) so I will no longer be flying blind. I hope things will improve with it.