6.Joystick Control

The first application I put together was pretty basic. It was meant to control your cheapest and most common RC car, one composed of two DC motors. But I did not want those cheap RC cars, but also the ones that use a servo motor and an electronic speed controller. This application does this and a bit more.

I first decided to go with an interface that assembles your regular two joystick controller:


When I started developing the logistic part of the application, I came up with a way for the user to modify the speed and steering ranges without having to touch the Arduino. The ranges can be specified in the Ranges tab:

I also decided to have the application control a tilt pan mounted on the RC car. This is done by simply toggling the third button. The turning ranges for the tilt pan can also be set from within the application:

The application can store different ranges under different profiles. This allows you to have a unique set of ranges for each one of you RC cars:

As with the first application, this one also makes use of the phone’s accelerometer to control the car:

A Bluetooth module’s range is quite an issue when controlling an expensive and fast RC car. The speeds these cars can reach mean that they can go out of the phone’s range in the blink of an eye. To solve this inconvenience, I decided to have the app send a continuous stream of commands to the car. The commands are sent every 50ms. Enough to not miss the user’s input, and keep the Arduino from overflowing with commands. The application echos the last command the user inputted. This continuous and repeated stream of commands is filtered in the Arduino. But just in case you do not need this feature, you can choose to have the app send a command to the car only when there's a change in either of the joystick's position.

Last but not least are the sensor readings. The application is able to receive six different sensor readings from the car. The first two are floating point readings. The remaining four are a single byte each, meaning that they can only be between 0 and 255:

 

In the code you will see that I have a variable called stetSize, and it is used with the first sensor reading. This variable and the reading from a simple voltage divider circuit are used to calculate the 9V battery’s voltage. Since the Arduino can’t measure its own voltage levels, you need a voltage divider for that. You do have to choose the correct resistor values. But that is not too difficult. All you need to do is get the ratio between the voltages across R1and R2 in your voltage divider. In my case a new 9 volt battery measures about 9.3V. I needed the voltage across R2 to be 5V (equal to the Arduino’s Vref), meaning that the remaining 4.3 volts will be across R1. The ratio between the values is 5V / 4.3V = 1.17. I now need to choose two resistor values that give me this value (or something close). I ended up choosing a 15kΩ for R1 and 18kΩ for R2. Their ratio is 18KΩ / 15Ω = 1.2 which is close enough:


To calculate the step size, I used the following formula:

Step size = (Vref/Resolution) * (Vbat/VR2)

Step size = (5V/1023) * (9.3V/5V)

Step size = 9.09 mV










I implemented this value in the code. After comparing the multimeter reading and the Arduino's calculation, I tweaked the calculated step size number until these two values were close. The final step size for my setup ended up being 9.04 mV. Be aware that this reading will only be accurate while the Arduino has a 5V power source to the Aref pin. When the 9 volt battery goes below a certain voltage (around 6.1 volts), it will make the Arduino’s regulator’s output voltage dip below 5V.

Going into the GUI Settings tab allows you to set the start and end labels for all six readings. This makes it easier to determine what reading belongs to what sensor:

https://lh4.ggpht.com/sfOzd6Vg7FMGxa3nIriW7mTeZNoQ0_c7TJca4kZ6MWdETF_1L6Ib8Wu93maPlZnNDQ=h900

This is my setup. A micro servo for the front steering, and an L293 for the back motor control:




This Fritzing sketch shows the elements I used:




Car_With_Sensors_Continuous

The Arduino code does all the filtering. It makes sure that the Arduino changes its pins’ state only if there is a variation in the incoming command stream. The program also detects when the car has lost connection to the phone. Since the application can receive six sensor readings, the last chunk of the program puts all the six readings into a 14 byte payload. A simple library I created for the L293 chip is implemented as well. It is enclosed in the zip folder containing this program. Make sure you add it to your Arduino library folder:


#include <Servo.h>
#include <L293.h>
#define pinForward 8    
#define pinBack 7      
#define pinSpeedForwardBack 6
#define pinFrontLights 2
#define pinBackLights 3
#define pinFrontSteering 10
//L293(pinForward, pinBack, pinFwdBakVel);
L293 redCar(pinForward,pinBack,pinSpeedForwardBack);
Servo leftRight;
byte commands[4] = {0x00,0x00,0x00,0x00};
byte prevCommands[4] = {0x01,0x01,0x01,0x01};
//Variables will be used to determine the frequency at which the sensor readings are sent
//to the phone, and when the last command was received. 
unsigned long timer0 = 2000;  //Stores the time (in millis since execution started)
unsigned long timer1 = 0;  //Stores the time when the last sensor reading was sent to the phone
unsigned long timer2 = 0;  //Stores the time when the last command was received from the phone
//14 byte payload that stores the sensor readings
byte three[14] = {0xee,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xcc};
//Constant used to caculate the 9V battery voltage (9.04 mV per step)
float stepSize = 9.04;
//The union allows you to declare a customized data type, in this case it can be either
//a float or a byte array of size 4. What we need is to store a float which is 4
//bytes long and retrieve each of the 4 bytes separately.
union u_sensor0{
  byte a[4]; 
  float b;
}sensor0;
union u_sensor1{
  byte c[4]; 
  float d;
}sensor1;
int i = 0;

void setup()
{      
  Serial.begin(115200); 
  pinMode(pinFrontLights, OUTPUT);
  pinMode(pinBackLights, OUTPUT);
  leftRight.attach(pinFrontSteering);
}

void loop()
{
  if(Serial.available()==4){
    timer2 = millis();  //Store the time when last command was received
    memcpy(prevCommands,commands,4);  //Storing the received commands  
    commands[0] = Serial.read();  //Direction
    commands[1] = Serial.read();  //Speed
    commands[2] = Serial.read();  //Angle
    commands[3] = Serial.read();  //Lights and buttons states
    /*
     Since the last byte yields the servo's angle (between 0-180), it can never be 255. At times, the two
     previous commands pick up incorrect values for the speed and angle. Meaning that they get the direction
     correct 100% of the time but sometimes get 255 for the speed and 255 for the angle.
     */
    if((commands[2]<=0xb4)&&((commands[0]<=0xf5)&&(commands[0]>=0xf1))){
      //Make sure that the command received involves controlling the car's motors (0xf1,0xf2,0xf3)
      if(commands[0] <= 0xf3){
        if(commands[0] == 0xf1){  //Check if the move forward command was received
          if(prevCommands[0] != 0xf1){  //Change pin state to move forward only if previous state was not move forward
            redCar.forward_1W(commands[1]);
            //Serial.println("Updated direction FWD");
          } 
        }
        else if(commands[0] == 0xf2){  //Check if the move back command was received    
          if(prevCommands[0] != 0xf2){  //Change pin state to move back only if previous state was not move back
            redCar.back_1W(commands[1]);
            //Serial.println("Updated direction BAK");
          }
        }
        else{  //Check if the stop command was received   
          if(prevCommands[0] != 0xf3){  //Change pin state to stop only if previous state was not stop
            redCar.stopped_1W();  
            //Serial.println("Updated direction STP");
          }
        }
        //Change speed only if new speed is not equal to the previous speed
        if(prevCommands[1] != commands[1]){
          redCar.setSpeed_1W(commands[1]); 
          //Serial.println("Updated speed");
        }         
        //Steer front wheels only if the new angle is not equal to the previous angle
        if(prevCommands[2] != commands[2]){
          leftRight.write(commands[2]); 
          //Serial.println("Updated angle");
        }        
      }
      else if(commands[0] == 0xf5){
        if(prevCommands[0] != 0xf5){
          //Stop everything
          redCar.stopped_1W();
          digitalWrite(pinFrontLights,LOW);
          digitalWrite(pinBackLights,LOW);
        }
      }
      else{
        //Here you put the code that will control the tilt pan (commands[0] == 0xf4)  
      }
      //Check the front/back lights and other toggles  
      if(prevCommands[3] != commands[3]){
        //Serial.println(commands[3],BIN);
        //Change the light/button states
        //               _______________________________________________
        //command[3] =  |  0  |  0  |  0  |  0  |  0  |  0  |  0  |  0  |  binary
        //              |_____|_____|_____|_____|_____|_____|_____|_____|
        //Buttons ---->  Front  Back  Horn   A     B     C     D     E  
        //Front lights
        if((bitRead(prevCommands[3],7))!=(bitRead(commands[3],7))){
          if(bitRead(commands[3],7)){
            digitalWrite(pinFrontLights,HIGH);
          }
          else{
            digitalWrite(pinFrontLights,LOW);
          }
        }
        //Back lights
        if((bitRead(prevCommands[3],6))!=(bitRead(commands[3],6))){
          if(bitRead(commands[3],6)){
            digitalWrite(pinBackLights,HIGH);
          }
          else{
            digitalWrite(pinBackLights,LOW);
          }
        }
        //Horn (using front lights to test)
        if((bitRead(prevCommands[3],5))!=(bitRead(commands[3],5))){
          if(bitRead(commands[3],5)){
            digitalWrite(pinFrontLights,HIGH);
          }
          else{
            digitalWrite(pinFrontLights,LOW);
          }
        }
      }     
    }
    else{
      //Resetting the Serial port (clearing the buffer) in case the bytes are not being read in correct order.
      Serial.end();
      Serial.begin(9600);
    }
  }
  else{
    timer0 = millis();  //Get the current time (millis since execution started)
    if((timer0 - timer2)>400){  //Check if it has been 400ms since we received last command
      //More tan 400ms have passed since last command received, car is out of range. Therefore
      //Stop the car and turn lights off
      redCar.stopped_1W(); 
      digitalWrite(pinFrontLights,LOW);
      digitalWrite(pinBackLights,LOW);
    }
    if((timer0 - timer1)>=477){  //Check if it has been 477ms since sensor reading were sent
      //Calculate the 9V's voltage by multiplying the step size by the step number (analogRead(0))
      //This value will be in mV, which is why it's multiplied by 0.001 to convert into Volts.
      sensor0.b = (analogRead(0) * stepSize) * 0.001; 
      //Break the sensor0 float into four bytes for transmission
      three[1] = sensor0.a[0];
      three[2] = sensor0.a[1];
      three[3] = sensor0.a[2];
      three[4] = sensor0.a[3];
      //Get sensor 2's reading
      sensor1.d = analogRead(1); 
      //Break the sensor1 float into four bytes for transmission
      three[5] = sensor1.c[0];
      three[6] = sensor1.c[1];
      three[7] = sensor1.c[2];
      three[8] = sensor1.c[3];
      //Get the remaining reading from the analog inputs
      three[9] = map(analogRead(2),0,1023,0,255);
      three[10] = map(analogRead(3),0,1023,0,255);
      three[11] = map(analogRead(4),0,1023,0,255);
      three[12] = map(analogRead(5),0,1023,0,255);
      //Send the six sensor readings
      Serial.write(three,14);
      //Store the time when the sensor readings were sent
      timer1 = millis();
    }
  }
}

As you can see in the figure below, the first byte (byte 0) indicates what the next two bytes (byte 1 and 2) are. If the first byte is 0xf1 – 0xf3, the next two bytes indicate the moving speed and front wheel’ steering angle. If the first byte is 0xf4, the next two indicate the tilt pan’s position. And if the first byte is 0xf5, the car is stopped and the front wheels are centered:


Byte #

0

1

2

3

Car’s

Direction

0xF1

Forward Speed

Front Wheels Angle

Button’ State

0xF2

Backward Speed

Front Wheels Angle

Button’ State

0xF3

Zero Speed

Front Wheels Angle

Button’ State

Tilt Pan

0xF4

Tilt Pan Left-Right

Tilt Pan Up-Down

Button’ State

Turn Off All

0xF5

Zero Speed

Center Front Wheels

Turn Off All

Size

1 Byte

1 Byte

1 Byte

1 Byte

 

4 Bytes

 

 The fourth and last byte (byte 3) stores the state of the buttons. This is shown bellow. Each bit in the last byte controls the car’s front lights, back lights, horn, and any other three devices connected to the car. There are two extra bits that are not used by the application (bit 1 represented by D and bit 0 represented by E).

 

MSB

 

 

 

 

 

 

LSB

Bit #

7

6

5

4

3

2

1

0

Button

Front Lights

Back Lights

Horn

A

B

C

D

E


The car can also take six different readings from its six analog inputs. Two readings are floating point numbers, while the remaining four are unsigned integers with a range of 0 to 255. The six readings are encapsulated and sent to the phone. The figure below shows how the data is encapsulated:

Byte #

13

12

11

10

9

8 to 5

4 to 1

0

Value

0xcc

Integer

0 to 255

Integer

0 to 255

Integer

0 to 255

Integer

0 to 255

Float number

Float number

0xee

 

Ending Byte

Analog5

Analog4

Analog3

Analog2

Analog1

Analog0

Leading Byte

Size

14 Bytes


I also decided to put together a remote control composed of a joystick and a master Bluetooth module:



It is not as good as the app since it can only be used with one car. But it works perfectly. The Arduino code is straight forward. Just map the readings coming from the joystick’s potentiometers to those needed to control the servo and L293 chip. The data stream from this controller is similar to that of the phone application's, continuous (every 50ms):

/*
/\ Vertical 1023
\/ Vertical 0
Center Ver=520
<- Horizontal 1023
-> Horizontal 0
Center Hor = 507
*/
int horizontalPot = 0;
int verticalPot = 0;
byte commands[4] = {0x00,0x00,0x00,0x00};

void setup(){
 Serial.begin(9600);
}

void loop(){
  verticalPot = analogRead(0);
  horizontalPot = analogRead(1);
 
  if(verticalPot >= 700){  //FORWARD
    commands[0] = 0xf1;
    commands[1] = (byte) map(verticalPot,700,1023,1,255);  //Speed         
  }
  else if(verticalPot <= 500){  //BACK
    commands[0] = 0xf2;
    commands[1] = (byte) map(verticalPot,0,500,255,1);  //Speed     
  } 
  else{  //STOP
    commands[0] = 0xf3;
    commands[1] = 0x00;  //Speed     
  }
 
  if(horizontalPot >=700){  //LEFT
    commands[2] = (byte) map(horizontalPot,700,1023,87,56);  //Angle  
  }
  else if(horizontalPot <= 500){  //RIGHT
    commands[2] = (byte) map(horizontalPot,500,0,89,122);  //Angle     
  }
  else{  //CENTER
    commands[2] = (byte) 88;  //Angle     
  }
  Serial.write(commands,4);
  delay(50);
}

Testing the car, Android applications, and the joystick control remote:

Download the application from the Google Play store: https://play.google.com/store/apps/details?id=com.andico.control.joystick

Since this application also lets you choose between a continuous and on change data stream, there are two code version. They are labeled accordingly. The code for the joystick also has a continuous and on change version. All the programs are listed below.
ċ
Car_With_Sensors_Continuous.zip
(7k)
Bluetooth Rc,
Aug 7, 2014, 10:19 AM
ċ
Car_With_Sensors_OnChange.zip
(7k)
Bluetooth Rc,
Aug 7, 2014, 10:19 AM
ċ
Joystick_Continuous.zip
(1k)
Bluetooth Rc,
Aug 4, 2014, 6:17 PM
ċ
Joystick_OnChange.zip
(1k)
Bluetooth Rc,
Aug 4, 2014, 6:17 PM
ċ
RC_circuit.fzz
(27k)
Bluetooth Rc,
Jul 22, 2014, 8:55 AM
ċ
joystic_schematic.fzz
(15k)
Bluetooth Rc,
Jul 22, 2014, 8:55 AM
Comments