RC channel Reading using Hardware Interrrupts

Hello Everyone!

I am trying to understand the logic of the following code which I have to use to control a quadcopter using Arduino mega 2560. I don't want to generate soft interrupt thats why I am trying to do it with hard interrupts. I have taken this code from (credits to Omar)

http://hobbylogs.me.pn/?p=110

The problem I am facing while understanding the code is in read_rc_rx() function and ISR for interrupts e.g. (void CH1_int_ISR() ).

I am new to arduino. I did work in PIC 18 interrupts earlier so I am having concept of interrupt. The thing I am unable to understand is the logic behind these calculations.
Any kind of help would be appreciating!
Regards.

#define dt 20         // [ms] Task time 



#define CH1_int 0     // Channel 1 interrupt # 
#define CH1_pin 2     // Respective channel Hardware interrupt pin number

#define CH2_int 1     // Channel 2 interrupt # 
#define CH2_pin 3     // Respective channel Hardware interrupt pin number

#define CH3_int 4     // Channel 3 interrupt # 
#define CH3_pin 19     // Respective channel Hardware interrupt pin number

#define CH4_int 5     // Channel 4 interrupt # 
#define CH4_pin 18    // Respective channel Hardware interrupt pin number

#define valid_pulse_limit 3000 // [uS] Valid output high pulse time limit for RC controller
#define max_high_time 1895 // [uS] Maximum expected high time
#define min_high_time 1090 // [uS] Minimum expected high time
 

unsigned long t=0;

void init_rc_rx();
void read_rc_rx();  

volatile unsigned long CH1_t=0, CH1_delta=0, CH2_t=0, CH2_delta=0, CH3_t=0, CH3_delta=0, CH4_t=0, CH4_delta=0 ;
float CH1,CH2,CH3,CH4;


// Interrupt ISRs

void CH1_int_ISR()
{
  if ((micros()-CH1_t) < valid_pulse_limit){
    CH1_delta = micros()-CH1_t;
  }
  CH1_t = micros();
}

void CH2_int_ISR()
{
  if ((micros()-CH2_t) < valid_pulse_limit){
    CH2_delta = micros()-CH2_t;
  }
  CH2_t = micros();
}

void CH3_int_ISR()
{
  if ((micros()-CH3_t) < valid_pulse_limit){
    CH3_delta = micros()-CH3_t;
  }
  CH3_t = micros();
}

void CH4_int_ISR()
{
  if ((micros()-CH4_t) < valid_pulse_limit){
    CH4_delta = micros()-CH4_t;
  }
  CH4_t = micros();
}

// Call able function

void init_rc_rx(){
  
  Serial.print("Channel 1 connected to pin number....\t");
  Serial.println(CH1_pin);
  Serial.print("Channel 2 connected to pin number....\t");
  Serial.println(CH2_pin);
  Serial.print("Channel 3 connected to pin number....\t");
  Serial.println(CH3_pin);
  Serial.print("Channel 4 connected to pin number....\t");
  Serial.println(CH4_pin);
  
  pinMode(CH1_pin, INPUT);
  pinMode(CH2_pin, INPUT);
  pinMode(CH3_pin, INPUT);
  pinMode(CH4_pin, INPUT);
  
  attachInterrupt(CH1_int, CH1_int_ISR, CHANGE);
  attachInterrupt(CH2_int, CH2_int_ISR, CHANGE);
  attachInterrupt(CH3_int, CH3_int_ISR, CHANGE);
  attachInterrupt(CH4_int, CH4_int_ISR, CHANGE);
}

void read_rc_rx(){
  CH1 = ((float)CH1_delta-(float)min_high_time)*100/(max_high_time-min_high_time);
  CH2 = ((float)CH2_delta-(float)min_high_time)*100/(max_high_time-min_high_time);
  CH3 = ((float)CH3_delta-(float)min_high_time)*100/(max_high_time-min_high_time);
  CH4 = ((float)CH4_delta-(float)min_high_time)*100/(max_high_time-min_high_time);

}






void setup()
{
  Serial.begin(115200); // Initlizing serial port before calling init_rc_rx() function 
  init_rc_rx();
   
}

void loop()
{ 
  t = micros()/1000;
  
  read_rc_rx();        // CH1, CH2, CH3 and CH4 float variables will be calculated with this function is called
    
  
 
  
  
  Serial.print(CH1);
  Serial.print("\t");
  Serial.print(CH2);
  Serial.print("\t");
  Serial.print(CH3);
  Serial.print("\t");
  Serial.print(CH4); 
  Serial.print("\t");
  Serial.println(((micros()/1000)- (float)t)*100/dt);
  
  
  while (dt > (micros()/1000)- t){
  // do nothing
  }
  
  
}

The ISR is being used to measure the high time (pulse width) of the incoming signal from your RC receiver. The PWM specification for RC stuff is generally stated as 1ms-2ms high pulse (1000 micro-2000 micro) with a 20ms (20k micro) low spacing. The ISR will trigger on state change (High to Low or Low to High)

So lets say we start with the 20K micro low and then transition to the high pulse which triggers the ISR

if ((micros()-CH1_t) < valid_pulse_limit)

The valid_pulse_limit is 3000 microseconds and we just had a 20K micro low so the if statement is not found to be true, therefore we skip whats inside the if and execute CH1_t = micros() which sets CH1_T equal to the current time (in microseconds) to use to measure how long until the next state change occurs. Lets say we are at neutral stick (1500 microsecond pulse), in 1500 microseconds the ISR will trigger again as the high pulse transitions to a low. The "if" will be found true meaning the next instruction is executed.

CH1_delta = micros()-CH1_t;

When the state changed from low to high we set CH1_t equal to the current time, now we use this to find out how much time has elapsed while the pulse was high by subtracting CH1_t from the current time (in our example here this would set CH1_delta equal to 1500).

CH1_t is again set to the current time but 20K microseconds elapses prior to the next state change so once again the if statement is false, the CH1_t is set to current time and ready to measure the next cycle. In this way the ISR is only returning the amount of time that the pulse is high (which is what is important for RC stuff).

As far as

CH1 = ((float)CH1_delta-(float)min_high_time)*100/(max_high_time-min_high_time);

This is being used to scale the returned time down to a value between 0 and 100, one could think of this as a percentage of stick throw. If you put 1500 in as CH1_delta you will get a value of around 51. I am guessing the author determined the actual minimum and maximum pulse widths for his equipment based on measurement and put those values in as min_high_time and max_high_time instead of just using 1000 and 2000 as the values.

Thankyou very much BH72. I got the logic behind these function with you help. I have a question.
As i stated, I am using Arduino Mega 2560 for making my own flight controller for the quadcopter X config. So I have to read minimum 6 channels to control mu quad. As well as I am using MPU 6050 (accelerometer + gyro sensor) connected to pin 20 and 21 using as I2C communication. so out of 6 hard interrupts, I am left with 4 Hard interrupts.

one solution is I would chose pinChangeInt library to generate soft interrupts, but what I am thinking, using 4 available hard interrupts and for rest of 2 channels, I use soft interrupts (pinChangeInt library). Is it doable? I mean is there any overhead for reading channels through soft interrupts? Do hard interrupts read channel more efficiently than the soft interrupts?

I have not personally used pinChangeInt library so I can not speak with any real authority on that, but I would assume that it would have a bit more overhead than actual hardware interrupts. Likely someone will be along with a better response to your pinChangeInt library question before long.

Let me pose another idea though. You stated that you need to monitor 6 channels, for a quad you are going to use 4 for control (pitch, roll, yaw, throttle), what are the other two going to be used for. If they are "accessory" channels (like used to activate lights, or a camera, or maybe adjust gyro gain, etc) that do not require near instant response you could possibly just get the status of these two channels several times a second.

Consider this, in your loop() you increment a counter every time through the loop, when the counter reaches 50 (or whatever) reset the counter and get the status of the channel like this..

unsigned long chanFiveIn = pulseIn(chanFivePin, HIGH);

This will return the same sort of result as the code used above for getting pulse width (1000-2000 microseconds). The problem with doing it this way is that under the right circumstances (started pulseIn at the same time the pulse just went low and with maximum pulse width) it would take around 22ms to return the result.

Also perhaps do not check both channel 5 and channel 6 in the same iteration of loop so as to avoid a possible 44ms delay in one iteration of loop, something like this..

counter++;
if(counter == 25 || counter == 75)
{
chanFiveIn = pulseIn(chanFivePin, HIGH);
}
if(counter == 50 || counter == 100)
{
chanSixIn = pulseIn(chanSixPin, HIGH);
counter = 0;
}

I suspect that soft interrupts would avoid some of the lag time associated with using pulseIn; however 22ms is not terribly long in the grand scheme of things.

I honestly do not know which way would be faster, an interrupt it going to trigger about 50 times a second and will need to be dealt with each time, whereas you could use the above method to check non critical channels fewer times each second, but each individual check would likely take longer.

Either way unless you plan to use I2C capable ESCs for motor speed control there is going to be a maximum refresh rate for the ESCs (around 450hz for non-I2C ESCs) so it is actually possible for your code to be too fast.

Thankyou very much for your suggested logic. It seems legit to me. I literally appreciate your effort for writing me such a great logic for reading rest of two channels. But I have little bit confusion though.

BH72:
Either way unless you plan to use I2C capable ESCs for motor speed control there is going to be a maximum refresh rate for the ESCs (around 450hz for non-I2C ESCs) so it is actually possible for your code to be too fast.

Yes I am using non-I2C ESCs. Can I still adapt your suggested logic for reading the rest of two channels?

ahsanraza:
Yes I am using non-I2C ESCs. Can I still adapt your suggested logic for reading the rest of two channels?

Yes, the logic will still work perfectly fine. My point in that statement was that even though you want fast code so your PID has plenty of time to handle things you do not have to worry an excessive amount about making sure no time is wasted. With a maximum of 2ms pulse width you are limited to 500 pulses per second (500 pulses x 2ms = 1 second), the maximum rate that your ESC is going to handle before it becomes confused will be something less than that.

So essentially if you are updating ESCs every time through loop() your loop will need to execute less than 500 times per second.

Also I kind of messed up the logic a little, should be more like
if(counter = 100)
{
counter = 0;
}

As the previous logic would reset counter at 50, it would never reach 100; however, it would still operate the same in practice.