Looking for help programming a RC car

Working on a simple RC car that is capable of propotional speed and steering control. I will be building the chassis of the car from LEGO technic and will be using LEGO Power Functions motors. For steering I am using a high-torque servo motor since the LEGO Power Functions servo motors move in steps.

I have converted an old pistol grip 2.4Ghz remote control to an Arduino Nano based remote control using a nRF24L01+ transceiver module and wired the throttle and steering pots to A0 and A1. The remote control is powered by the original 4xAA battery pack.

On the car I am using an Arduino Uno with another nRF24L01+ transceiver module for comminucation, a L298N H-bridge speed controller for controlling the motors speed and a servo motor for steering. The H-bridge and Arduino are powered by a 12v Li-Ion battery (probably later upgraded to a 14,8v Li-Ion battery for more speed.

I am able to establish a connection between the two Arduinos and have sucessfully loaded some codes from other creators to control the servo with the remote control.

Yet my knowledge of writing and understanding these codes is very limited and I have not found a code that works for my setup (combining servo steering and H-bridge speed control over nRF24 transmission).

Now I am in need of some help to complete the codes. I need the Servo to be set up for proportional steering and the motors set up for proportional speed control. Also I want to be able to calibrate and map my analog inputs.

Receiver / Car code:


/* Radio Receiver
    Adeept UNO with Motor Shield V2
    Radio module: nRF24L01 + PA + LNA with antenna built in -USE 3V !!!!  built in, 5V is fine ONLY with shield or adaptor, or bypass capacitor, see specs
    L298N - 7.4 V input - two 46850 batteries , use whatever you like .
    Can be done with any pair of UNOs - Written by Gallax

    L298N is grounded to Uno.

    Servo pin 3 or P4 on Adeept shield
    CE 9  CSN 10 for Adeept shield

      ////////////L298N CONNECTIONS  ON ADEEPT SHIELD OR UNO ////////////////
                     IN1    IN2    IN3    IN4       SPD1    SPD2
                      8      7      2      4         5       6

    I didnt use RF24 Adaptor, shield has one built in. So this is universal for Unos.
 *   *   ////////////////////// RF24 Adaptor or use this pinout :) /
    __________________________________
    || 3V  ||  10 ||   11  ||  NC   ||           NC - not connected.
    || VCC-||-CSN-||- MOSI-||- IRQ  ||
    ||_____||_____||_______||_______||
    || GND-||-CE -||- SCK -||- MISO ||
    || GND ||  9  ||  13   ||   12  ||
    ||_____||_____||_______||_______||


    ///////////////////////////Servo connection //////////////////////////

    Servo is connected to 5V, GND, pin is 2 (search for servo.attach for reference)
    Adeept shield requires pin 2 = P4 - yes this was quite a job.

*/

#include <SPI.h>
#include <nRF24L01.h>       // include RF24 libraries
#include <RF24.h>           // This will need some tweaking. Excellent job. 
#include <Servo.h>          // Include Servo library 

Servo servo;               // creates servo object to control servo
// twelve servo objects can be created on most boards

RF24 radio(9, 10);                           // CE, CSN
const uint64_t pipe = 0xE8E8F0F0E1LL;       // sets channel
// const byte address[6] = "00001";         // alternative

int data[8];                   // create dataset array 8 bit


int enA = 5;   // speed control A
int in1 = 8;                  // Motor A connections
int in2 = 7;

int enB = 6;   // speed control B
int in3 = 2;                  // Motor B connections
int in4 = 4;

int xDir = 90;          // initial direction for servo later

////////////////////////////////////////////////////////////////////////

void setup() {

  Serial.begin(9600);            // start serial monitor for debugging

  radio.begin();                     // starts the radio
  radio.openReadingPipe(0, pipe);    // sets this RF24 as receiver
  radio.setPALevel(RF24_PA_MIN);    // sets radio signal strength
  radio.setDataRate(RF24_250KBPS);   // sets datarate to 250 kbps
  radio.startListening();            // starts listening for data


  /// ///////////// Set all the motor control pins to outputs/////////////////
  pinMode(enA, OUTPUT);     // speed control motor A on L298N
  pinMode(enB, OUTPUT);     // speed control motor B on L298N
  pinMode(in1, OUTPUT);     // motor A output - will become forward high
  pinMode(in2, OUTPUT);     // motor A output - will become forward low
  pinMode(in3, OUTPUT);     // motor B output - will become forward high
  pinMode(in4, OUTPUT);     // motor B output - will become forward low

  servo.attach(3);        //sets servo pin to 2 = P4 on Adeept Shield

}

//////////////////LISTEN, READ, MAPPING AND PRINTING DATA////////////////////

void loop() {

  radio.startListening();             // receiver starts listening

  if (radio.available()) {            // if the radio has connection

    radio.read(&data, sizeof(data));    // read all in the dataset, sizeof is for numeric purposes

    int x = map(data[1], 0, 1024, 0, 180);   // creates integer x, maps data[0], 0 is the first integer i used for the array, other than starting at 1
    int y = map(data[0], 0, 1024, 0, 255);   // now data is next in line at  1.
    // maps data from 0 - 1024 --- converted by me to 0 - n where n = 180 or 255

    Serial.print("x:");       // serial print text  for reading
    Serial.println(data[0]);  // (note only one with ln)print data, if fails, check connections. (note only one with ln)
    Serial.print("y:");       //prints text
    Serial.print(data[1]);    // prints the next piece of data
    Serial.print("\t");       // i dunno, youll see why i didnt change this

    /////////////////////IF DATA RECEIVED - SERVO CONTROLS//////////////////////

    if ( data[1] == 0 )  {          // if data 0 is equal to 0. servo turns left
      xDir = 180;                    // see unit circle
      servo.write(xDir);             // TURN
      delay(15);                     // small wait
    }

    if ( data[0] > 600 )  {           // if data is greater than or equal to 600
      xDir = 0;                       // dir is 0
      servo.write(xDir);             // TURN
      delay(15);                     // rinse repeat
    }

    if (data[0] == 329 or data[0] == 328 ) {      // more if data for x axis, (steering)
      xDir = 90;                                   // 90 is straight
      servo.write(xDir);                           // no turn
      delay(15);                                  // small wait
    }

    /////////////////////IF DATA RECEIVED - MOTOR CONTROLS///////////////////////

    if ( data[0] >= 530 )  {

      analogWrite(enA, 255);
      analogWrite(enB, 255);
      // Set motors to maximum speed
      // For PWM maximum possible values are 0 to 255

      // Turn on motor A & B
      digitalWrite(in1, HIGH);
      digitalWrite(in2, LOW);           //  GO MOTORS!
      digitalWrite(in3, HIGH);
      digitalWrite(in4, LOW);

    }

    /********************************************************************/

    else {
      analogWrite(enA, 0);
      analogWrite(enB, 0);
      digitalWrite(in1, LOW);            //  STOP MOTORS
      digitalWrite(in2, LOW);
      digitalWrite(in3, LOW);
      digitalWrite(in4, LOW);
    }

    /////////////////////////////////////////////////////////////////////

    if ( data[0] <= 500 )  {
      analogWrite(enA, 255);
      analogWrite(enB, 255);
      digitalWrite(in1, LOW);                   //  BACKWARDS MOTORS
      digitalWrite(in2, HIGH);                // if failed Check your wires, motor faces up (red wire on top)
      digitalWrite(in3, LOW);
      digitalWrite(in4, HIGH);
    }

    /////////////////////////////////////////////////////////////////////


    else {         // waiting for connection, or NOT connected

    }                               // delete this else statement if it becomes an eyesore
  }
}





////////////////      END       /////////////////

Transmitter / Remote control code:


/* Radio Transmitter
    Elegoo Uno with starter kit shield.
    nRF24L01 + PA + LNA with antenna - USE 3V!!! you can only use 5V with adaptor
    i used a 9v battery directly into the Uno.
    Can be done with any pair of UNOs - Written by Gallax

   ////////////////////////// Joystick connection /////////////////////////////

    GND       -
    5V or 3V  -
    X out     - A0
    Y out     - A1
    SW(button)- NC - not connected

     //////RF24 Adaptor or use this pinout directly into the Uno:) / ////////

    __________________________________
    || 3V  ||  8  ||   11  ||  NC   ||           NC - not connected.
    || VCC-||-CSN-||- MOSI-||- IRQ  ||         note: remember CE 7  CSN 8
    ||_____||_____||_______||_______||
    || GND-||-CE -||- SCK -||- MISO ||
    || GND ||  7  ||  13   ||   12  ||
    ||_____||_____||_______||_______||




*/


#include <SPI.h>
#include <nRF24L01.h>                    // include RF24 libraries
#include <RF24.h>

RF24 radio(7, 8);                       // CE, CSN
const uint64_t pipe = 0xE8E8F0F0E1LL;   // set channel
// const byte address[6] = "00001";     // also sets channel

int xPin = A0;                       // integer for joystick, x axis, analog pin A0
int yPin = A1;                       // integer for joystick, y axis, analog pin A5

// int x;
//  int y;
int data[8];                         // forming datagroup, 8 bits is enough for car

//////////////////////////////////////////////////////////////////////////////

void setup() {

  Serial.begin(9600);                  // start serial monitor for debugging

  radio.begin();                       // start radio
  radio.openWritingPipe(pipe);         // this is the controller
  radio.setPALevel(RF24_PA_HIGH);      // High power
  radio.setDataRate(RF24_250KBPS);     // data rate 250 kb/s
  radio.stopListening();               // stops listening to transmit

}

/////////////////////////////////////////////////////////////////////////////

void loop() {

  xPin = analogRead(A0) - 15;                   // read x pin from joystick + calibration offset potmeter
  yPin = analogRead(A1) + 81;                  //  read y pin from joystick + calibration offset potmeter

  data[0] = xPin;                 // defines xPin which is A0 as data
  data[1] = yPin;                 // defines yPin which is A1 as data


  radio.write(&data, sizeof(data));          // write 8 bits of data to receiver
  // no mapping required for transmitter


  Serial.print("x:");          // text for debugging
  Serial.println(data[0]);    // prints data  notice this value is print line
  Serial.print("y:");        //print the values with to plot or view
  Serial.print(data[1]);     // prints the next piece of data
  Serial.print("\t");       // i dunno it works
}

typical RC transmitters transmit a sequence of pulse, 1-2ms which are separated and passed to each individual servo. yes, in the case of a DC motor, the pulse is translated into a PWM signal.

you could do something similar (i.e. transmit pulses), looks like the nRF24 is designed for this

the transmitter will need to monitor the pistol grip inputs. i'm guessing they are pots that can be read using analogRead(). those values are used to generate the spaced pulses to the nRF24.

if this approach works, the receiver needs to recognize the start of the pulse sequence (probably a longer time between pulse, the time the dureation o each pulse and then regenerate a pulse to the servos and translate the pulse corresponding to the motor to a PWM signal using analogWrite()

but maybe there's another approach to using the nRF24

Sounds like you’ve got a bit of learning…
You can give it your best shot and ask along the way , or try here.
Jobs & Paid consultancy

We’re good to help you learn stuff, but rarely will the forum write a complete solution… then you don’t ‘learn’ anything?

If you post the code that you made tests with The users here can take a look into this code.

Another aproach than pulses is to transmit for bytes of data.
The meaning of the four bytes is:
byte 1 and byte 2 "form" a 16 bit signed value variable int whicn can have values from -32767 to + 32767
a negative sign means drive backwards a positive sign means drive forward

byte 3 and byte 4 "form" a 16 bit signed value variable int whicn can have values from -32767 to + 32767
negative value means turn to the left / positive value turn to the right.

If this approach needs a lot of modification or just a few modifications depends on the code that you used to test.

so please
You can post code by using this method that adds the code-tags
There is an automatic function for doing this in the Arduino-IDE
just three steps

  1. press Ctrl-T for autoformatting your code
  2. do a rightclick with the mouse and choose "copy for forum"
  3. paste clipboard into write-window of a posting

best regards Stefan

You are absolutely right, Stefan! Thank you for replying and helping me to properly post the code here. I have added it to my initial post.

I am using a code that uses the same hardware that is also written for a RC car. The difference is that this code is not written for use with proportional steering and speed control. That part is what I want to integrate into this code.

Again, thanks!

Ok. I looked into your code. As an exercise:
what do you assume where in the transmitter-code is that place that does starts the sending the data "over the air"?

posting your assumption will give information about your knowledge-level and how to adapt future answers to your knowledge-level.

Even if you feel completely unsecure make a decision and post one line of code
that you think "this might be the line of code that

starts the sending the data "over the air"

best regards Stefan

Thank you Stefan,

I guess that is over here is where the data is being send in the loop section:

radio.write(&data, sizeof(data)); // write 8 bits of data to receiver

Best Regards,

Luuk

Hi Luuk,

Yes 100% right. That is the place where the data is send over the air.

another two questions:
do you know what an array is?

where is the line of code that reads in the position of the potentiometer?

Hi Stefan,

To be honest I had to look up what an array is but found out that an array is a consecutive group of memory locations that are of the same type.

The lines where the position of the potmeter is read is:

xPin = analogRead(A0) - 15;

The -15 is what I subtracted as calibration so that the X-axis pin reads out neatly at a value of 512 when in the center (default) position. It now reads out between a value of 0 and 1023.

Please correct me If I am wrong, I figured out that at the receiver end this value is to be mapped between a value of 0 and 180 for steering servo and for the H-bridge speed controllers to a range of 0 and 255.

That is basicly as far as my knowledge goes :sweat_smile:

Yes and this is correct.

For a proportional control you need values between 0 and 180 for the left/right servo
where 90 is straight forward

and values -255...0......+255 for the H-bridge where 0 is car stops

reading in the potentiometer results in values 0..1023
with 512 +- calibration is the middle-position.

the .write()-function simply transmits a number of bytes

this write-function starts reading the bytes given as the first parameter "&data"

and the number of bytes it shall read is given as the second parameter "sizeof(data)"

sizeof() is a function too. That looks up what size does the variable "data" have which means how many bytes does the variable have.

So using

sends the right number of bytes automatically

On sender and receiver the variables used to send and receive must match 100% to make sure that the data is sorted the right way after receiving in the receiver-code

This means the variables must be of the same type and in case of an array have the same number of elements

The ADC delivers values 0..1023. For numbers big as 1023 the variable type integer must be used
This means two integers are sufficient for the information
position of left/right-poti
position of forward/backward-poti
this means element 0 and element 1 of the array is used to store the poti-value.

The code defines an array of 8 elements which leaves room for future extensions like switch on/off lights or a horn, limiting speed to 20% or whatever

on the receiver-side the values of left/right-poti must be mapped from 0..1023 to 0 to 180

The values of the forward/backward-poti must be processed into
values between 0..512 drive backwards where 0 means maximum-speed backwards and 512 means speed zero

and

values between 512 and 1023 means drive forward where 512 means speed zero and 1023 means maximum speed forward

for testing the principle I recommend to use the serial monitor instead of the real hardware

This means adding code to the receiver-code that prints the values of the involved variables to the serial monitor

In the receiver-code: Where are the lines of code that receive the data?
and which array-element holds the left/right-value and which array-element holds the forward/backward-value?

EDIT:

I go on writing in this post to avoid too much consecutive posts from me.

This code is pretty small and is executed very fast by an Ardunio.
This means we can afford to use several steps inbetween from the raw value 0..1023 to
servopos and throttle.

To make the code easier to read for later maintaining / modifying the code you should define additional functions for

calculate servo-angle from raw value
transform value 0..1023 to throttle forward / backward
functions for setting the L298-board inputs for forward / backward driving
a function for setting the speed

You should code this one thing at a time.
Start modifying the code and whenever a problem occurs ask here in the forum
always post your complete sketch, write a description of what the could should do
and
write a description of what the code does instead

best regards Stefan

Stefan you are an absolute legend!!! :star_struck:

Made a lot of progress on the receiver code the past few hours! I am now using the raw potmeter data as servo input and it actually works great!

I have also managed to insert a bit of speed control code from 'DroneBot Workshop' into my receiver code.

The last bit I am struggling with is forward / backward control. My throttle potmeter has a forward range of 512-1000 and a backward range of 240-512 (about half the range). I want to be able to go the same speed forward as backwards. My intuition is to map forward / backwards differently using the "if" command. You can see it in the code below:


/* Radio Receiver
    Adeept UNO with Motor Shield V2
    Radio module: nRF24L01 + PA + LNA with antenna built in -USE 3V !!!!  built in, 5V is fine ONLY with shield or adaptor, or bypass capacitor, see specs
    L298N - 7.4 V input - two 46850 batteries , use whatever you like .
    Can be done with any pair of UNOs - Written by Gallax

    L298N is grounded to Uno.

    Servo pin 3 or P4 on Adeept shield
    CE 9  CSN 10 for Adeept shield

      ////////////L298N CONNECTIONS  ON ADEEPT SHIELD OR UNO ////////////////
                     IN1    IN2    IN3    IN4       SPD1    SPD2
                      8      7      2      4         5       6

    I didnt use RF24 Adaptor, shield has one built in. So this is universal for Unos.
 *   *   ////////////////////// RF24 Adaptor or use this pinout :) /
    __________________________________
    || 3V  ||  9  ||   11  ||  NC   ||           NC - not connected.
    || VCC-||-CSN-||- MOSI-||- IRQ  ||
    ||_____||_____||_______||_______||
    || GND-||-CE -||- SCK -||- MISO ||
    || GND ||  8  ||  13   ||   12  ||
    ||_____||_____||_______||_______||


    ///////////////////////////Servo connection //////////////////////////

    Servo is connected to 5V, GND, pin is 3

*/

#include <SPI.h>
#include <nRF24L01.h>       // include RF24 libraries
#include <RF24.h>           // This will need some tweaking. Excellent job. 
#include <Servo.h>          // Include Servo library 

Servo servo;               // creates servo object to control servo
// twelve servo objects can be created on most boards

RF24 radio(8, 9);                           // CE, CSN
const uint64_t pipe = 0xE8E8F0F0E1LL;       // sets channel
// const byte address[6] = "00001";         // alternative

int data[8];                   // create dataset array 8 bit

// Motor A

int enA = 5;
int in1 = 2;                  // Motor A connections
int in2 = 4;


// Motor B

int enB = 6;
int in3 = 2;                  // Motor B connections
int in4 = 4;


//Motor Speed Values

int MotorSpeed1 = 0;
int MotorSpeed2 = 0;

// Servo Motor

int xDir = 90;          // initial direction for servo later
int xThreshhold = 3;     // steering deadzone threshold to prevent random wobbling of the servo

////////////////////////////////////////////////////////////////////////

void setup() {

  Serial.begin(9600);            // start serial monitor for debugging

  radio.begin();                     // starts the radio
  radio.openReadingPipe(0, pipe);    // sets this RF24 as receiver
  radio.setPALevel(RF24_PA_MIN);    // sets radio signal strength
  radio.setDataRate(RF24_250KBPS);   // sets datarate to 250 kbps
  radio.startListening();            // starts listening for data


  /// ///////////// Set all the motor control pins to outputs/////////////////
  pinMode(enA, OUTPUT);     // speed control motor A on L298N
  pinMode(enB, OUTPUT);     // speed control motor B on L298N
  pinMode(in1, OUTPUT);     // motor A output - will become forward high
  pinMode(in2, OUTPUT);     // motor A output - will become forward low
  pinMode(in3, OUTPUT);     // motor B output - will become forward high
  pinMode(in4, OUTPUT);     // motor B output - will become forward low

  servo.attach(3);        //sets servo pin to 2 = P4 on Adeept Shield

}

//////////////////LISTEN, READ, MAPPING AND PRINTING DATA////////////////////

void loop() {

  radio.startListening();                    // receiver starts listening

  if (radio.available()) {                   // if the radio has connection

    radio.read(&data, sizeof(data));         // read all in the dataset, sizeof is for numeric purposes

    int x = map(data[1], 81, 954, 0, 180);   // data 1 = steer left/right control
    int y = (data[0]);                       // data 0 = forward + backword + speed control

    Serial.print("x:");                      // serial print text  for reading
    Serial.println(data[1]);                 // (note only one with ln)print data, if fails, check connections. (note only one with ln)
    Serial.print("y:");                      //prints text
    Serial.print(data[0]);                   // prints the next piece of data

    /////////////////////IF DATA RECEIVED - SERVO CONTROLS//////////////////////

    servo.write(x);

    /////////////////////IF DATA RECEIVED - MOTOR CONTROLS///////////////////////


    int Speedcontrol1 = (y);
    int Speedcontrol2 = (y);

    MotorSpeed1 = Speedcontrol1;
    MotorSpeed2 = Speedcontrol2;


    // Move Motors Forward

    if ( (y) >= 520 )  {


      // Set Motor A forward
      digitalWrite(in1, HIGH);
      digitalWrite(in2, LOW);

      // Set Motor B forward

      digitalWrite(in3, HIGH);
      digitalWrite(in4, LOW);

      // Convert to range of 0-255

      MotorSpeed1 = map(MotorSpeed1, 512, 1000, 0, 255);
      MotorSpeed2 = map(MotorSpeed2, 512, 1000, 0, 255);

      if (MotorSpeed1 < 30)MotorSpeed1 = 0;
      if (MotorSpeed2 < 30)MotorSpeed2 = 0;

      if (MotorSpeed1 > 255)MotorSpeed1 = 255;
      if (MotorSpeed2 > 255)MotorSpeed2 = 255;

      analogWrite(enA, MotorSpeed1);
      analogWrite(enB, MotorSpeed2);

    }

    // Move Motors Backwards

    if ( (y) <= 504 )  {

      // Set Motor A backward
      digitalWrite(in1, LOW);
      digitalWrite(in2, HIGH);

      // Set Motor B backward

      digitalWrite(in3, LOW);
      digitalWrite(in4, HIGH);

      // Convert to range of 0-255

      MotorSpeed1 = map(MotorSpeed1, 512, 240, 0, 255);
      MotorSpeed2 = map(MotorSpeed2, 512, 240, 0, 255);

      if (MotorSpeed1 < 30)MotorSpeed1 = 0;
      if (MotorSpeed2 < 30)MotorSpeed2 = 0;

      if (MotorSpeed1 > 255)MotorSpeed1 = 255;
      if (MotorSpeed2 > 255)MotorSpeed2 = 255;

      analogWrite(enA, MotorSpeed1);
      analogWrite(enB, MotorSpeed2);

    }

    /********************************************************************/

    else {
      analogWrite(enA, 0);
      analogWrite(enB, 0);
      digitalWrite(in1, LOW);            //  STOP MOTORS
      digitalWrite(in2, LOW);
      digitalWrite(in3, LOW);
      digitalWrite(in4, LOW);


    }                               // delete this else statement if it becomes an eyesore
  }
}





////////////////      END       /////////////////

The problem is that I can now only move motors backwards. If I remove the "Move Motors Backwards" part of the code I can only move forward. I take the assumtion that when the "if" condition is not met, that part of the code gets skipped, right? So why does "Move Motors Forward" part get skipped in this code even when (y) >= 520?

naaah legend is an exaggeration

whenever your condition

if ( (y) <= 504 )

is not true the code inside the else

    else {
      analogWrite(enA, 0);
      analogWrite(enB, 0);
      digitalWrite(in1, LOW);            //  STOP MOTORS
      digitalWrite(in2, LOW);
      digitalWrite(in3, LOW);
      digitalWrite(in4, LOW);

gets executed. This else gets executed in case of

(y) >= 520

You can solve this by writing a third if-condition that checks if you are inbetween
505 to 511
this if-condition is only true if you are really inside the middle-deadband

best regards Stefan

Aaah that makes perfect sense actually. Everything works like I imagined it would now! Thank you so much!! :innocent: :vulcan_salute:
I will post a link to my project hub page here when I am finished if you are interested. :slight_smile:

best regards Luuk