RC Controller to UNO, need some guidence

Hey Folks!
I need some help completing this code I found from Andy Vikers YawTube, converting RC signals to usable data in my uno. My scope of c coding is not good enough to figure out how to extract the arrays from his code and control my motors.

I've a L298n pushing two dc gear motors, and I need help sending pwm to pins 5,6,
then on/off for the directions, in1, in2, in3, in4.

Here's what I have so far:
Thanks!!

/*
  Arduino RC Controller Input
  ---------------------------
  Lesson 2: Refine and Translate RC Controller Input 

  Adds the following features:

    Failsafe    - if the RC controller goes out of range, react
    Inversion   - if your control stick goes the wrong way we can handle it
    Deadzone    - dont react until the sticks are intentionally pushed
    Translation - map our values to a sensible range like -100 to 100


  Read more about this example on the blog post at https://www.andyvickers.net/

  Author: Andy Vickers
  Downloaded From: https://github.com/andyman198/ArduinoRCControllerInput
  
*/
// Set the port speed for host communication
#define SERIAL_PORT_SPEED 9600

// Set the size of the arrays (increase for more channels)
#define RC_NUM_CHANNELS 5

// Set up our receiver channels - these are the channels from the receiver
#define RC_CH1 0  // Right Stick LR
#define RC_CH2 1  // Right Stick UD
#define RC_CH3 2  // Left  Stick UD
#define RC_CH4 3  // Left  Stick LR
#define RC_CH5 4  // Left  Stick LR


// Set up our channel pins - these are the pins that we connect to the receiver
// use this website as a guide:
//    https://www.arduino.cc/reference/cs/language/functions/external-interrupts/attachinterrupt/


// ARDUINO MEGA
//#define RC_CH1_INPUT  18 // receiver pin 1
//#define RC_CH2_INPUT  19 // receiver pin 2
//#define RC_CH3_INPUT  20 // receiver pin 3
//#define RC_CH4_INPUT  21 // receiver pin 4
//#define RC_CH4_INPUT  2 // receiver pin 5
//#define RC_CH4_INPUT  3 // receiver pin 6


// ARDUINO Nano 33 IoT
 #define RC_CH1_INPUT 2   // receiver pin 1
 #define RC_CH2_INPUT 3   // receiver pin 2
// #define RC_CH3_INPUT 4   // receiver pin 3
// #define RC_CH4_INPUT 5   // receiver pin 4
// #define RC_CH5_INPUT 6   // receiver pin 5
// #define RC_CH5_INPUT 7   // receiver pin 6
// #define RC_CH5_INPUT 8   // receiver pin 7
// #define RC_CH5_INPUT 9   // receiver pin 8
// #define RC_CH5_INPUT 10  // receiver pin 9
// #define RC_CH5_INPUT 11  // receiver pin 10
// #define RC_CH5_INPUT 12  // receiver pin 11
// #define RC_CH5_INPUT 13  // receiver pin 12

// ARDUINO PRO MICRO / LEONARDO
#define RC_CH1_INPUT 2  // receiver pin 1
#define RC_CH2_INPUT 3  // receiver pin 2
#define RC_CH3_INPUT 1  // receiver pin 3
#define RC_CH4_INPUT 0  // receiver pin 4
#define RC_CH5_INPUT 7  // receiver pin 5


// Set up some arrays to store our pulse starts and widths
uint16_t RC_VALUES[RC_NUM_CHANNELS];
uint32_t RC_START[RC_NUM_CHANNELS];
volatile uint16_t RC_SHARED[RC_NUM_CHANNELS];


// to the extent possible, all the variables are scaleable, you will need to add more values
// here if you use a higher number of channels
uint16_t RC_LOW[RC_NUM_CHANNELS] = { 1092, 1092, 1092, 1092, 1092 };
uint16_t RC_MID[RC_NUM_CHANNELS] = { 1508, 1508, 1508, 1508, 0 };
uint16_t RC_HIGH[RC_NUM_CHANNELS] = { 1924, 1924, 1924, 1924, 1924 };

// The RC Channel mode helps us know how to use and refine the signal
// Settings are:
// 0 = a joystick with a centerpoint (deadzone in middle)
// 1 = a throttle that goes from low to high (deadzone at start)
// 2 = a a switch (either on or off)
uint16_t RC_CHANNEL_MODE[RC_NUM_CHANNELS] = { 0, 0, 0, 0, 1 };

// Do we need to invert the input? 1 = yes, 0 = no
// Use this if your joystick goes the wrong way
uint16_t RC_INVERT[RC_NUM_CHANNELS] = { 1, 0, 1, 0, 1 };

// what should we set the value to if we cant see the RC controller?
uint16_t RC_FAILSAFE_VALUE[RC_NUM_CHANNELS] = { 1508, 1508, 1508, 1508, 1092 };

// a place to store our mapped values
float RC_TRANSLATED_VALUES[RC_NUM_CHANNELS];

// some boundaries for our mapped values
float RC_TRANSLATED_LOW[RC_NUM_CHANNELS] = { -100, -100, -100, -100, 0 };
float RC_TRANSLATED_MID[RC_NUM_CHANNELS] = { 0, 0, 0, 0, 0 };
float RC_TRANSLATED_HIGH[RC_NUM_CHANNELS] = { 100, 100, 100, 100, 100 };

// What percentage deadzone is allowed? values in percent e.g. 10 = 10%
uint16_t RC_DZPERCENT[RC_NUM_CHANNELS] = { 30, 30, 30, 30, 5 };





// Setup our program
void setup() {
//here ive defined my pins......................................................................
pinMode(enA, OUTPUT); //SET PINS OUTPUTS FOR L298N DRIVER INPUTS
pinMode(in1, OUTPUT);
pinMode(in2, OUTPUT);

pinMode(enB, OUTPUT);
pinMode(in3, OUTPUT);
pinMode(in4, OUTPUT);


Channel 1 = RC_TRANSLATED_VALUES[0]
Channel 2 = RC_TRANSLATED_VALUES[2]
Channel 2 = RC_TRANSLATED_VALUES[3]




  // Set the speed to communicate with the host PC
  Serial.begin(SERIAL_PORT_SPEED);

  // Set our pin modes to input for the pins connected to the receiver
  pinMode(RC_CH1_INPUT, INPUT);
  pinMode(RC_CH2_INPUT, INPUT);
  pinMode(RC_CH3_INPUT, INPUT);
  pinMode(RC_CH4_INPUT, INPUT);
  pinMode(RC_CH5_INPUT, INPUT);

  // Attach interrupts to our pins
  attachInterrupt(digitalPinToInterrupt(RC_CH1_INPUT), READ_RC1, CHANGE);
  attachInterrupt(digitalPinToInterrupt(RC_CH2_INPUT), READ_RC2, CHANGE);
  attachInterrupt(digitalPinToInterrupt(RC_CH3_INPUT), READ_RC3, CHANGE);
  attachInterrupt(digitalPinToInterrupt(RC_CH4_INPUT), READ_RC4, CHANGE);
  attachInterrupt(digitalPinToInterrupt(RC_CH5_INPUT), READ_RC5, CHANGE);
}

unsigned long current = 0;
unsigned long prev = 0;
const unsigned long interval = 100000UL;

bool failsafeActive = true;

void loop() {

  // read the values from our RC Receiver
  rc_read_values();

  // first lets see if we are in a failsafe condition
  processFailsafe();

  // invert the signal if we want to (e.g. the joystick goes the wrong way)
  rc_invert_values();

  // only make changes to the signal if we arent in a failsafe condition
  if (failsafeActive == false) {
    rc_deadzone_adjust();
  }

  // map the radio values to the range we want e.g. -100 to 100
  rc_translate_values();


  // Now its over to you!
  /* 
     Now you can use the newly translated values in your project, you can access them like this:

     Channel 1 = RC_TRANSLATED_VALUES[0]
     Channel 2 = RC_TRANSLATED_VALUES[1]
     Channel 3 = RC_TRANSLATED_VALUES[2]
     etc
 
     assuming you set them to a range you can use, you could position a servo
     or drive a motor or even pass them through to an odrive or ROS

                here's an example:
                #include <Servo.h>
                Servo servo1; int servoPin1 = 9;
                Servo servo2; int servoPin1 = 10;
                Servo servo3; int servoPin1 = 11;

                void setup(){
                  servo1.attach(servoPin1);
                  servo2.attach(servoPin2);
                  servo3.attach(servoPin3);
                }

                void loop(){
                  servo1.write(RC_TRANSLATED_VALUES[0]);
                  delay(1000);
                  servo2.write(RC_TRANSLATED_VALUES[1]);
                  delay(1000);
                  servo3.write(RC_TRANSLATED_VALUES[2]);
                  delay(1000);
                }

  */




 


  // keep track of time
  current = micros();

  // This is our plotter Chart output, we only do it every so often or the plotter moves too fast
  if (current - prev >= interval) {
    prev += interval;

    // loop through all channels and display value
    // note: the plotter only holds 8 values, if you add more than 5 plus the min, mid and max here it
    // will not show all your plots!

    for (int i = 0; i < RC_NUM_CHANNELS; i++) {
      Serial.print("CH");
      Serial.print(i);
      Serial.print(":");
      Serial.print(RC_TRANSLATED_VALUES[i]);
      Serial.print(",");
    }

    //Use Ch1 as a reference point for low and high and plot them
    Serial.print("LOW:");
    Serial.print(RC_TRANSLATED_LOW[0]);
    Serial.print(",");
    Serial.print("HIGH:");
    Serial.print(RC_TRANSLATED_HIGH[0]);

    // if failsafe is active, display on the serial but also plot it as a visual aid
    if (failsafeActive == true) {
      Serial.println(",FAILSAFE:2500");
    } else {
      Serial.println("");
    }
  }
}

// Thee functions are called by the interrupts. We send them all to the same place to measure the pulse width
void READ_RC1() {
  Read_Input(RC_CH1, RC_CH1_INPUT);
}
void READ_RC2() {
  Read_Input(RC_CH2, RC_CH2_INPUT);
}
void READ_RC3() {
  Read_Input(RC_CH3, RC_CH3_INPUT);
}
void READ_RC4() {
  Read_Input(RC_CH4, RC_CH4_INPUT);
}
void READ_RC5() {
  Read_Input(RC_CH5, RC_CH5_INPUT);
}


// This function reads the pulse starts and uses the time between rise and fall to set the value for pulse width
void Read_Input(uint8_t channel, uint8_t input_pin) {
  if (digitalRead(input_pin) == HIGH) {
    RC_START[channel] = micros();
  } else {
    uint16_t rc_compare = (uint16_t)(micros() - RC_START[channel]);

    RC_SHARED[channel] = rc_compare;
  }
}

// this function pulls the current values from our pulse arrays for us to use.
void rc_read_values() {

  noInterrupts();
  memcpy(RC_VALUES, (const void *)RC_SHARED, sizeof(RC_SHARED));
  interrupts();
}

void rc_invert_values() {

  // loop through the channels

  for (int i = 0; i < RC_NUM_CHANNELS; i++) {
    // do we need to invert?
    if (RC_INVERT[i] == 1) {

      if (RC_CHANNEL_MODE[i] == 0) {

        // if this is a joystick with a midpoint
        RC_VALUES[i] = (RC_HIGH[i] + RC_LOW[i]) - RC_VALUES[i];

      } else if (RC_CHANNEL_MODE[i] == 1) {

        // if this is a throttle
        RC_VALUES[i] = RC_HIGH[i] - (RC_VALUES[i] - RC_LOW[i]);
      }
    }

    // a little clipping to make sure we dont go over or under the bounds

    // clip the high range so it doesnt go over the max
    if (RC_VALUES[i] > RC_HIGH[i]) {
      RC_VALUES[i] = RC_HIGH[i];
    }

    // clip the low range so it doesnt go under the min
    if (RC_VALUES[i] < RC_LOW[i]) {
      RC_VALUES[i] = RC_LOW[i];
    }
  }
}

void rc_translate_values() {

  // Loop through all our channels
  for (int i = 0; i < RC_NUM_CHANNELS; i++) {

    // translate the RC channel value into our new number range
    RC_TRANSLATED_VALUES[i] = translateValueIntoNewRange((float)RC_VALUES[i], (float)RC_HIGH[i], (float)RC_LOW[i], RC_TRANSLATED_HIGH[i], RC_TRANSLATED_LOW[i]);
  }
}
// here is what I've added so far, not sure what I'm doing wrong........................................
Channel 1 = RC_TRANSLATED_VALUES[0]
Channel 2 = RC_TRANSLATED_VALUES[1]
Channel 3 = RC_TRANSLATED_VALUES[2]

if(RC_TRANSLATED_VALUES[0] > 1520) {
  analogWrite(enA,200);
 digitalWrite(in1,HIGH);
 digitalWrite(in2,LOW);
} else{
  digitalWrite(in1,LOW);
  digitalWrite(in2,HIGH);
}







void processFailsafe() {

  //temporarily reset failsafe while we calculate where we are
  failsafeActive = false;

  // see if any channels have failed
  for (int i = 0; i < RC_NUM_CHANNELS; i++) {

    // Lets convert our range into -100 to 100 so we can compare against our deadzone percent
    float newval = translateValueIntoNewRange((float)RC_VALUES[i], (float)RC_HIGH[i], (float)RC_LOW[i], 100.0, 0);

    if (abs(newval) > 105.0) {

      //failsafe active, we are way out of range of where we should be, likely the controller
      // lost battery or went out of range
      failsafeActive = true;
    }
  }

  // if we triggered a failsafe, we need to set all the channels to their failsafe value
  // in my experience, only channel 1 goes out of bounds in these situations
  if (failsafeActive == true) {
    for (int i = 0; i < RC_NUM_CHANNELS; i++) {
      RC_VALUES[i] = RC_FAILSAFE_VALUE[i];
    }
  }
}

void rc_deadzone_adjust() {

  // Lets convert our range into -100 to 100 so we can compare against our deadzone percent
  for (int i = 0; i < RC_NUM_CHANNELS; i++) {
    // first off, we cant divide by zero so lets get that out the way

    float newval = 0;

    if (RC_CHANNEL_MODE[i] == 0) {
      // if this is a joystick with a midpoint, our deadzone should be around the middle
      newval = translateValueIntoNewRange((float)RC_VALUES[i], (float)RC_HIGH[i], (float)RC_LOW[i], 100.0, -100.0);

      if (abs(newval) < RC_DZPERCENT[i]) {
        // reset to the midpoint if we are in the deadzone
        RC_VALUES[i] = RC_MID[i];
      }

    } else if (RC_CHANNEL_MODE[i] == 1) {
      // if this is a throttle, our deadzone should be at the low point
      newval = translateValueIntoNewRange((float)RC_VALUES[i], (float)RC_HIGH[i], (float)RC_LOW[i], 100.0, 0.0);

      if (abs(newval) < RC_DZPERCENT[i]) {
        // reset to the low point if we are in the deadzone
        RC_VALUES[i] = RC_LOW[i];
      }
    }
  }
}

float translateValueIntoNewRange(float currentvalue, float currentmax, float currentmin, float newmax, float newmin) {
  // Use this formula to work out where we are in the new range
  // NewValue = (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
  //
  // this formula was lovingly stolen from https://stackoverflow.com/questions/929103/convert-a-number-range-to-another-range-maintaining-ratio

  return (((currentvalue - currentmin) * (newmax - newmin)) / (currentmax - currentmin)) + newmin;
}

That code is not designed for an Uno. An Uno only has 2 pins that can be used for interrupts but that code wants more. That is why it is set up for a Mega or a Pro Micro or Nano 33 IoT.

You have an awful lot of channels going on there, for two motors. As @blh64 says, that code is written for boards with more interrupt pins than the Uno has, but two things:

  • that program was meant to be tailored by viewers like you to their use case (which you attempted but why are you reading five channels for two motors?)
  • should be fine on an Uno since you only need two RC channels. What do you think ON/OFF looks like in PWM for your motors? Do you mean neutral?

What are you building? Car? Boat? Robot?

You don't need to "extract" anything, but you could benefit from actually reading the code.

I did, here's

  // Now its over to you!
  /* 
     Now you can use the newly translated values in your project, you can access them like this:

     Channel 1 = RC_TRANSLATED_VALUES[0]
     Channel 2 = RC_TRANSLATED_VALUES[1]
     Channel 3 = RC_TRANSLATED_VALUES[2]
     etc

where the author suggests you place any code you want that uses the values in the RC_TRANSLATED_VALUES array.

I suppose those assignments coukd be called extractions, but no one does. The author uses access, which would the more common term.

Try printing there, if I missed where there is already printing. After you see the values and how they change with sticks input from your TX, start learning C so you can make stuff happen that is informed by those values.

HTH

a7

Thanks Guys!
This is all very insightful.
Blh I didn't even consider that this might not work with the uno. I have a connect rp2040, where I think I can name my interrupts, maybe I should try that.

This a for a lawn mower, ha, I know. I've a large yard and not money for a zero turn, and I thought this would be a fun project. I'm almost there! I just need to figure out this code. Ultimately I might want to use gps and program it for the large sections, but I just need to get it moving first.
I'll have to figure out how to slow and reverse the rear wheels to turn, but I'm hoping I can figure that out with the 'if' command.

I'll need a few more channels, at least for starting the engine, and stop, maybe program mode and run program, but I might do this from the box and not remote.
As far as PWM, I was thinking of 0-255 for speed, controlled by the throttle, and ran thru code to manipulate each wheel for the steering.

I'm still lost on how to code this,

thanks again,
CA

How do you plan to zero-turn (differential (skid) steer) that mower? When that turns, the undriven wheels will skip-skate without at least Ackermann steering (which obviously isn't zero turn anymore), won't it?

There are plenty of folks who have done this project. I have not. I have made heavier "robots" (200 plus pounds) but I use hobby RC aircraft TX/RX to control those and the channel mixing is done "in house" on the Spektrum transmitter settings itself.

If you are planning on remote starting this gasoline engine and anything else, you might as well get a multichannel RC aircraft TX/RX and call it a day for now. That's how the BattleBots folks seem to do it, that's how lots of guides do it and that's what the Youtuber you're following was showing.

So regarding this

I think you're looking more toward RC type signals which, in Arduino world either correspond to 0-180 degrees as seen on most standard hobby servos, or something like lSignalOut.writeMicroseconds(1000); rSignalOut.writeMicroseconds(1000);
which in this case, happen to correspond to stopped in my use case (throttle stick, single channel mixed at low stick via Arduino to the two ESCs of an airboat I made.)

Is that a motor controller on the end of the Gilipsu project box with the red, yellow and black wires coming out?

Thanks for the response 31'.
That is a rectifier, I'm planning on using the engine to drive an alternator, giving me on board power.
I'm planning on having the steering by slowing one of the wheels, an perhaps floating the front wheels for sharper turns.
I think you might be right about the multi channel tx. I saw a video where a better one uses a bus to avoid using those interrupts, but I thought I could pull it off with what I had. Maybe that's the spektrum tx you're referring to...

1 Like

There are lots of RC combos out there, I have experience (and have been satisfied with) Spektrum, but I'll say they are pricier than maybe need be.

I shill for nobody, only endorse what I have tried and liked. What I do like about my 8 channel Spektrum is that it has a memory to save lots of Rx's so one transmitter drives two robots, one boat, two planes and a hexacopter (well, I disassembled it but it still could in theory send the signals lol).

If you are looking for motor controllers, I have had great success with Dimension Engineering Sabertooth 2x60 motor drivers. DIP switch protocol control if you want to go straight RC, Serial, whatever. I have only used the straight RC for these but can confirm the 2x60 will handle my 200 pound robot with ease, even on hills (modified electric wheelchair style). There are lots of options, not saying the 2x60 is the best, I don't know, but again, I've been very satisfied and been using the one I mentioned since about 2015 (I have two actually, both work as advertised)

https://www.dimensionengineering.com/products/sabertooth2x60

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.