Friday, September 9, 2016

Arduino is fun - great for custom Radio Control signal handling

I've been having some fun with arduino. They're quite cheap boards, with arduino nano coming in at ~$3 per board. That's $3 - THREE. That's pretty awesome for a small amount of custom processing power. Unlike a board like the Raspberry PI which has to run a full Linux kernel, this just runs whatever script you write. As such, the timings are very consistent - it's excellent for handling servo PWN signals where you need to measure pulses from ~1000-2000 microseconds accurately. From my experience the resolution was 4μs.

This makes it excellent for managing radiocontrol PWM signals in complex RC vehicles. 



That wrapped up red component in my latest RC vehicle (another hobby, I build RC cars...) is an arduino that reads in steering and another channel and processes it to output a new signal that lets me choose if I want the truck to have 4 wheel steering, or crabwalk (both wheels point the same way making it go mostly sideways).




Definitely one of the more interesting vehicles I've built, and a decent summer project (though I did start last year wrecking the transmission pushing myself in a chair). 

This actually started out with me receiving a non-working unit for handling quadsteer that I had purchased on amazon. When using the unit, the servos lacked power, wouldn't center, and were slow. I used an oscilloscope to diagnose the signal and saw the pulses were 40 milliseconds apart. For Radio control PWM signals, the leading edge should be 20 milliseconds apart. What was essentially happening was the server would get a signal and act on it for 20 milliseconds, and then not do anything for the next 20 milliseconds.

So I turned to arduino nano in the hopes to making my own. Below I've setup that arduino nano on the breadboard to mix signals as I'd like. Each square on the oscilloscope is 10 ms, so my working prototype is showing the proper waveform. This is what was wrapped up in electrical tape in the first picture.




Below is the code I wrote to handle this. The idea is to read two channels - the steering and a spare channel to know if to invert the steering between front and rear. Since I'm reading the time of the pulse, I need a little math to normalize 1000-2000μs to -1 to 1 and back again. This lets me multiply the channels so I can then smoothly transition between crabwalk/quadsteer. 

Should be noted we're using interrupts to gather input channel data. This is a non-blocking method for gathering the input. I just requires us to determine the time of the pulse by subtracting the time the clock had at the signal's leading edge.



#include <Servo.h> 

volatile unsigned long leadingedge1;
volatile unsigned long leadingedge2;
volatile int pulsetime1; 
volatile int pulsetime2;


//declare servo pins
int servoin1  = 2; // pin 2 - steering
int servoin2  = 3; // pin 3 - inversion channel
int servoout1 = 9; // output read servo is pin 9, front is pin 2
Servo rearservo;

int ch1_1=1500; //last 3 values are stored
int ch1_2=1500;
int ch1_3=1500;

int ch2_1=1500;
int ch2_2=1500;
int ch2_3=1500;

int execute=0;

void setup()
{
  pinMode(servoin1, INPUT);      // sets the digital pin 1 as input
  pinMode(servoin2, INPUT);      // sets the digital pin 1 as input
  pinMode(servoout1, OUTPUT);    // sets the digital pin 9 as output
  pinMode(servoout2, OUTPUT);    // sets the digital pin 9 as output
  leadingedge1 = 0;
  leadingedge2 = 0;
  pulsetime1 = 1500;
  pulsetime2 = 1500;
  attachInterrupt(0, chan1in1, CHANGE);
  attachInterrupt(1, chan1in2, CHANGE);
  rearservo.attach(servoout1);
 // Serial.begin(115200);        // for debugging
}

int mmode(int a,int b,int c){ //extremely basic meant to rid spikes from interference.
  int d=(a+b+c)/3;            //either return mode, or average of last 3 values
  if (a == b){
    d=a;
  } else {
    if (a == c){
      d=a;
    } else {
      if (b == c){
        d=b;
      }
    }
  }
  return d;
}

void dostuff(){
  execute=0;
  long product = 1500+((((long)pulsetime1-1500)*((long)pulsetime2-1500))/450);
  // dividing by 500 would be more exact, but this gives a little extra turn
if (product > 2200) { product=2200; } else { if (product < 800) { product=800; } } /* Serial.print("P1.");   Serial.print(pulsetime1);   Serial.print("--P2.");   Serial.print(pulsetime2);   Serial.print("--x.");   Serial.print(product);   Serial.println("--T."); */ rearservo.writeMicroseconds(product); } void chan1in1() { if(digitalRead(servoin1) == HIGH) { leadingedge1 = micros(); } else { if(leadingedge1 > 0) {
      pulsetime1 = ((volatile int)micros() - leadingedge1)-24;
      //24 us added from other operations? center needed normalizing to 1500
if ((pulsetime1>800)&(pulsetime1<2200)){ leadingedge1 = 0; ch1_3=ch1_2; ch1_2=ch1_1; ch1_1=pulsetime1; } pulsetime1=mmode(ch1_1,ch1_2,ch1_3); execute=1; } } } void chan1in2() { if(digitalRead(servoin2) == HIGH) { leadingedge2 = micros(); } else { if(leadingedge2 > 0) { pulsetime2 = ((volatile int)micros() - leadingedge2)-24; if ((pulsetime2>800)&(pulsetime2<2200)){ leadingedge2 = 0; ch2_3=ch2_2; ch2_2=ch2_1; ch2_1=pulsetime2; } pulsetime2=mmode(ch2_1,ch2_2,ch2_3); } } } void loop() { delay(3); //delay is non blocking if (execute==1) { delay(3); noInterrupts();
    dostuff();  //do stuff after the receiver has sent all pulses
    //technically we're using the receiver clock to time when we process data.
interrupts(); } }

No comments:

Post a Comment