Introduction
This post describes a remote-controlled car I have been working on in conjunction with my children. I describe below the various modules used in the final design, in the hope that the various techniques will be useful to others making something similar. Rather than launching into a major circuit description for the whole thing, the individual components will be described, so that you might incorporate bits and pieces into your own designs.
Finished vehicle:
Prototyping shield (plugged into Arduino Uno underneath it) with various parts labelled:
Li-Po 7.4V 2S 3300 mAH battery taped to underneath:
Transmitter board (not in a box yet) consisting of Arduino Uno connected to a breadboard with a Wii Nunchuk adapter, and the transmitter module (circled in green):
Motor controller board
To translate the logic level signals into enough power to drive motors we need a controller board, like this one:
The board has "H-Bridge" chips to drive the motors, plus extra logic to detect the amount of current being drawn (by measuring the current flow through the 4 large, blue, "current sense" resistors). This is amplified to a voltage that can be read on an analog port.
It also has logic to read the rotary encoders attached to the wheels, and "or" together the encoder outputs so you can get a single interrupt from each one. The interrupt (or encoder) output can be used to detect how fast the wheels are actually turning.
Test before assembling ...
My advice is to check the transmitter and receiver on a breadboard before trying to solder everything together. Believe me, it's frustrating when you put it all together, and try to test, and nothing happens! It could be:
- Faulty transmitter
- Faulty receiver
- Transmitter and receiver on different frequencies
- Faulty wiring
- Coding error (on either of the Arduinos)
- Wrong length antenna
Below I describe how I went about testing both the transmitter and receiver ...
Transmitter
This is the transmitter board, as you can see it is pretty small:
Reverse side:
It's pretty straightforward to wire it ... you just connect +5V (VCC), Ground, and serial data to the data pin. You can also attach an optional antenna.
Details at:
http://www.tato.ind.br/files/TX-C1.pdf
Suggested antenna lengths (from the receiver documentation):
- About 23 cm for 315 MHz
- About 17 cm for 434 MHz
Receiver
The receiver is a bit wider:
Reverse side:
Again, you just connect +5V, Ground, and data emerges from the data pin. You can also attach an optional antenna.
Test the transmitter
On a breadboard, connect up 5V, ground and an antenna. Plug the data pin into the Tx pin (D1) on the Arduino.
The yellow wire is the antenna.
This test sketch will continuously output all possible bytes to it:
void setup ()
{
Serial.begin (600);
} // end of setup
void loop ()
{
for (int i = 0; i < 256; i++)
Serial.write ((byte) i);
} // end of loop
Test the receiver
On a breadboard, connect up 5V, ground and an antenna.
We can now hook up an oscilloscope to the data pin and check that something is happening. The yellow line is the data going into the transmitter and the blue one is what we are getting on the receiver...
Whilst it is nice to see something happening, the receiver isn't getting exactly what the transmitter is sending. I found that by removing the antenna from the transmitter the problem went away. Possibly they were too close to work reliably.
Once that was fixed, the results looked like this:
Check with logic analyzer
It's all very well seeing 0s and 1s, but are they correct? Hooking up the data pin to the logic analyzer shows that we are indeed getting sequential bytes:
Connect receiver to Arduino
So, job done, right? We just connect the receiver the the Arduino and we can start sending data. Well, not quite. After connecting the receiver data pin to D0 (the Rx pin) of the Arduino, the oscilloscope suddenly shows this:
It appears that something on the Rx pin is interfering with the data.
On a hunch, I connected the receiver up to pin D8, and use the NewSoftSerial library to do a "software serial" read. This time the scope showed this:
It seems that the pull-up resistors used by NewSoftSerial (and also by the USB chip) are "too strong" for the receiver.
Operational Amplifier (op-amp) as a buffer
So we need to add in a "buffer" chip. I chose to use an LM358 op-amp, and set it up as a comparator. By using two 10K resistors as a voltage divider, the op-amp will output "full voltage" if the incoming voltage is more than 2.5V, and "zero voltage" if it is less than 2.5V.
Op-amp added to breadboard:
And connected to the Arduino:
Now it all works!
In the final version I added a switch between the op-amp and the Arduino Rx (D0) pin - otherwise you can't program the Arduino via the USB port any more, because the op-amp output is too strong for the USB chip. Also, an LED connected between the output and 5V, via a 1K resistor, is designed to light when there is data there. This is because with async serial data is present if the line is low, and there is no data if the line is high.
Receiver test sketch
To test, we can connect it up to pin D8, and use NewSoftSerial to read from the receiver, and echo back what it got to the serial monitor:
#include <NewSoftSerial.h>
NewSoftSerial truck (8, 9); // receive pin, transmit pin
void setup ()
{
Serial.begin (115200);
truck.begin (600);
} // end of setup
void loop ()
{
if (!truck.available ())
return;
byte c = truck.read ();
Serial.print (c, HEX);
Serial.println ();
} // end of loop
Error detecting protocol
Sending serial data through the air is likely to be fraught with problems (noise, drop-outs) so we need to assure ourselves that the data arrives reliably. There are various ways to do this, I used the "RS485" library I developed for sending serial data via a RS485 link.
Details here:
http://www.gammon.com.au/forum/?id=11428
Without getting too bogged down at this stage about where the data is coming from, here is a small example of how you can use this library:
// Example transmitter
#include <WProgram.h>
#include <RS485_protocol.h>
const int WIRELESS_BAUD_RATE = 600;
void setup ()
{
Serial.begin (WIRELESS_BAUD_RATE);
} // end of setup
// callbacks for the sendMsg function
void fWrite (const byte what)
{
Serial.write (what);
} // end of fWrite
int fAvailable ()
{
return Serial.available ();
} // end of fAvailable
int fRead ()
{
return Serial.read ();
} // end of fRead
void loop ()
{
// get data from nunchuk, joystick, switches etc.
byte joy_x_axis = 42;
byte joy_y_axis = 33;
// message for other end
byte msg [2] = { joy_x_axis, joy_y_axis };
// send it
sendMsg (fWrite, msg, sizeof msg);
} // end of loop
The sendMsg function sends the data (in my case, the X and Y position of the nunchuk) to the remote gadget (my wireless car). The function attaches a header (0x02), trailer (0x03) and sumcheck to ensure that the data is correct.
The receiver (eg. on the remote-controlled car) just calls recvMsg to get the data sent by the other end. If no message (or the message was corrupted in some way) then we just return, to re-enter the main loop, waiting for another one.
// Example receiver
#include <WProgram.h>
#include <RS485_protocol.h>
const int WIRELESS_BAUD_RATE = 600;
// callback routines
void fWrite (const byte what)
{
Serial.print (what);
} // end of fWrite
int fAvailable ()
{
return Serial.available ();
} // end of fAvailable
int fRead ()
{
return Serial.read ();
} // end of fRead
void setup ()
{
Serial.begin (WIRELESS_BAUD_RATE);
} // end of setup
void loop ()
{
byte msg [2];
if (!recvMsg (fAvailable, fRead, msg, sizeof (msg)))
return;
byte joy_x_axis;
byte joy_y_axis;
joy_x_axis = msg [0];
joy_y_axis = msg [1];
// move car appropriately here
} // end of loop
Low voltage detector
The next issue, which we discovered whilst driving around scaring the cat, was that the battery on the car could get dangerously low.
So, a method of detecting low voltage was necessary. Since the car was powered by a Li-Po battery (two cells in series) its valid range was 7.4V to 8.4V. Now 8.4V is too much to plug into an analog port, so we need to get it down to the range 0 to 5V. Thus, a simple voltage divider will do it:
Now the voltage on pin A2 will be the battery voltage, halved. So we now need a simple calculation to see if the battery is too low:
const float LOW_BATTERY_VOLTAGE = 7.5;
const byte BATTERY_CHECK = 2; // A2
// where the motors are connected to
const byte LEFT_MOTOR = 5;
const byte RIGHT_MOTOR = 6;
// warning LED
const byte RED_LED = 8;
void setup ()
{
// low battery warning LED
pinMode (RED_LED, OUTPUT);
} // end of setup
void loop ()
{
int voltage = analogRead (BATTERY_CHECK);
// 1024 would be 5V on the pin
// we have a voltage divider so the read voltage is half the battery
float volts = voltage / 1024.0 * 5.0 * 2.0;
if (volts < LOW_BATTERY_VOLTAGE)
{
analogWrite (LEFT_MOTOR, 0); // stop motors
analogWrite (RIGHT_MOTOR, 0);
digitalWrite (RED_LED, HIGH); // flash LED as warning
delay (50);
digitalWrite (RED_LED, LOW);
delay (950);
return; // don't try to use motors now
}
// rest of loop here
} // end of loop
The above code reads in the voltage from the battery voltage divider. Since it returns 1024 for "full" voltage, then we divide by 1024, multiply by 5 (since full voltage is 5) and then double it to allow for the voltage divider.
Then if the resulting voltage is too low (eg. less than 7.5 volts) we turn the motors off, flash the "warning" LED, and exit the loop.
Reverse voltage protection
Another idea to save a lot of expense is to handle someone plugging the battery in backwards.
Afrotechmods did a great tutorial on this:
http://www.youtube.com/watch?v=IrB-FPcv1Dc
The idea here is that, rather than just using a diode (which has a substantial voltage drop of around 0.7V) you use a P-channel MOSFET. The basic circuit is this:
As he explains, configured in this way, the MOSFET only allows forward conduction if the battery is plugged in the correct way.
In the case of the FQP27P06 transistor, the figure for RDS(on) is typically 0.055 ohms, so at a drain of 5A, the voltage drop would only be 5 * 0.055 = 0.275V. However most diodes have a forward voltage drop of around 0.7V, so that is a much larger voltage drop (and hence, more heat to be dissipated).
Of course, with lower current (eg. 2A) then the voltage drop over the MOSFET will be correspondingly lower (eg. 0.11V) compared to the fixed voltage drop of 0.7V with the diode).
Overload detection
In an earlier version (where the driving was done by an algorithm) I had a "current sense" hooked up. This uses the current sense resistor on the motor board, which has voltage drop over the resistor amplified and sent to analog pin A0. By reading that you can work out how much current is flowing into the motors, and thus if they are working too hard.
const byte CURRENTA = 0; // A0
const byte CURRENTB = 1; // A1
const int CURRENT_LIMIT = (1024 / 5) * 2.6; // amps
...
if (analogRead (CURRENTA) > CURRENT_LIMIT)
{
analogWrite (LEFT_MOTOR, 0); // stop motors
analogWrite (RIGHT_MOTOR, 0);
return; // don't try to use motors now
}
The example code shows how, in the main loop, you can detect the current being (say) over 2.6 amps, and then shut the motors down if so.
The current sense pin is documented to provide 1V per 1A motor drain, thus 5A would be a reading of 1024 on pin A0.
Reading the rotary encoders
If you want to see how fast the wheels are actually turning, you can connect up the rotary encoder "interrupt" outputs to pins D2 and D3. Then the code snippet below shows how you can count interrupts in an ISR (interrupt service routine). By comparing counts over a time period you can work out what speed the wheels are doing. Or, the ticks themselves just show how far you have gone (of course the wheels might be spinning, so that isn't totally reliable).
// rotary encoders - read wheel speed
const byte LEFT_ENCODER = 8;
const byte RIGHT_ENCODER = 9;
const byte INTERRUPTA = 0; // that is, pin 2
const byte INTERRUPTB = 1; // that is, pin 3
volatile unsigned long ticksA;
volatile unsigned long ticksB;
unsigned long start_timing;
int speedA, speedB;
// Interrupt Service Routine for a change to encoder pin A
void isrA ()
{
ticksA++;
} // end of isrA
// Interrupt Service Routine for a change to encoder pin B
void isrB ()
{
ticksB++;
} // end of isrB
void setup ()
{
attachInterrupt (INTERRUPTA, isrA, CHANGE);
attachInterrupt (INTERRUPTB, isrB, CHANGE);
start_timing = millis ();
// other setup stuff ...
}
void loop ()
{
// work out speed over 10th second
if (millis () - start_timing > 100)
{
// grab the data before it changes
noInterrupts ();
unsigned long interval = millis () - start_timing;
speedA = ticksA * 1000L / interval;
speedB = ticksB * 1000L / interval;
ticksA = ticksB = 0;
start_timing = millis ();
interrupts ();
}
// other stuff here ...
} // end of loop
Emergency Stop
One thing that became obvious during testing was that, if something went wrong, the vehicle might power itself away into some dangerous situation, like heading for a flight of stairs.
To avoid this we built in a test that, if no data was received from the radio in (say) one second, then the program would stop the motors.
const unsigned long TIME_BEFORE_WE_GIVE_UP = 1000; // ms
...
byte msg [2];
if (!recvMsg (fAvailable, fRead, msg, sizeof (msg)))
{
// no command for a second? stop!!!
if (millis () - lastCommand >= TIME_BEFORE_WE_GIVE_UP)
{
analogWrite (LEFT_MOTOR, 0); // stop motors
analogWrite (RIGHT_MOTOR, 0);
}
return;
}
// remember when we got a command
lastCommand = millis ();
This gives us an "out" if the car gets out of range, the transmitter fails, or there is too much radio interference.
Transmitter sketch
The transmitter program is fairly simple. Below is the complete sketch for it, including the (fairly simple) code to read the various buttons and controls on the Wii Nunchuk:
[EDIT] See further down for more up-to-date version which uses Manchester encoding rather than async serial.
// Remote controlled vehicle transmitter
// Author: Nick Gammon
// Date: 14th January 2012
#include <WProgram.h>
#include <Wire.h>
#include "RS485_protocol.h"
const int NUNCHUK_ADDRESS = 0x52;
const int WIRELESS_BAUD_RATE = 600;
const unsigned long TIME_BEFORE_WE_DEFINITELY_SEND = 250; // ms
const unsigned long TIME_BETWEEN_NUNCHUK_READS = 100; // ms
const byte LED = 13;
void setup ()
{
Serial.begin (WIRELESS_BAUD_RATE);
Serial.println ();
Wire.begin(); // join i2c bus as master
Wire.beginTransmission (NUNCHUK_ADDRESS);
Wire.send(0x40); // initialize nunchuk
Wire.send(0x00);
Wire.endTransmission();
pinMode (LED, OUTPUT);
digitalWrite (LED, LOW);
} // end of setup
unsigned long lastNunchukRead;
byte joy_x_axis;
byte joy_y_axis;
int accel_x_axis;
int accel_y_axis;
int accel_z_axis;
byte z_button;
byte c_button;
// Encode data to format that most wiimote drivers except
// only needed if you use one of the regular wiimote drivers
byte nunchuk_decode_byte (byte x)
{
return (x ^ 0x17) + 0x17;
}
boolean getNunchukData ()
{
if (millis () - lastNunchukRead < TIME_BETWEEN_NUNCHUK_READS)
return false; // too soon
Wire.requestFrom (NUNCHUK_ADDRESS, 6);// request data from nunchuk
// request next lot
Wire.beginTransmission(NUNCHUK_ADDRESS);
Wire.send(0);
Wire.endTransmission();
// no data from nunchuk
if (Wire.available () < 6)
return false;
joy_x_axis = nunchuk_decode_byte (Wire.receive());
joy_y_axis = nunchuk_decode_byte (Wire.receive());
accel_x_axis = nunchuk_decode_byte (Wire.receive()) * 2;
accel_y_axis = nunchuk_decode_byte (Wire.receive()) * 2;
accel_z_axis = nunchuk_decode_byte (Wire.receive()) * 2;
// the sixth byte contains bits for z and c buttons.
// it also contains the least significant bits for the accelerometer data
// so we have to check each bit of it
byte extra = nunchuk_decode_byte (Wire.receive());
z_button = 0;
c_button = 0;
// buttons
if ((extra >> 0) & 1)
z_button = 1;
if ((extra >> 1) & 1)
c_button = 1;
// invert the buttons, so 1 is pressed
z_button ^= 1;
c_button ^= 1;
if ((extra >> 2) & 1)
accel_x_axis += 2;
if ((extra >> 3) & 1)
accel_x_axis += 1;
if ((extra >> 4) & 1)
accel_y_axis += 2;
if ((extra >> 5) & 1)
accel_y_axis += 1;
if ((extra >> 6) & 1)
accel_z_axis += 2;
if ((extra >> 7) & 1)
accel_z_axis += 1;
lastNunchukRead = millis ();
return true;
} // end of getNunchukData
void fWrite (const byte what)
{
Serial.write (what);
}
int fAvailable ()
{
return Serial.available ();
}
int fRead ()
{
return Serial.read ();
}
// ---------------- MAIN LOOP -------------------
byte old_joy_x_axis;
byte old_joy_y_axis;
unsigned long lastTransmitTime;
void loop ()
{
if (!getNunchukData ())
return;
// if we recently sent, and data is the (almost the) same, don't bother
if ((old_joy_x_axis & 0xFE) == (joy_x_axis & 0xFE) &&
(old_joy_y_axis & 0xFE) == (joy_y_axis & 0xFE)) // data same?
{
int X = map (joy_x_axis, 31, 230, -100, 100);
int Y = map (joy_y_axis, 28, 222, -100, 100);
if (abs (X) < 5)
X = 0;
if (abs (Y) < 5)
Y = 0;
// only a short time elapsed, or sending "stand still"
if ((millis () - lastTransmitTime < TIME_BEFORE_WE_DEFINITELY_SEND)
|| (X == 0 && Y == 0)) // don't keep sending "stand still"
return; // no change
}
byte msg [2] = {joy_x_axis, joy_y_axis};
digitalWrite (LED, HIGH);
sendMsg (fWrite, msg, sizeof msg);
digitalWrite (LED, LOW);
lastTransmitTime = millis ();
old_joy_x_axis = joy_x_axis;
old_joy_y_axis = joy_y_axis;
} // end of loop
To try to minimize the amount of data we are sending at the slow rate of 600 baud the transmitting sketch checks if we are going to send the same thing (give or take the low-order bit) and if so, doesn't send it. However because of the "emergency stop" code on the vehicle, we also make sure we send something every 1/4 of a second, otherwise the car might stop if you held your thumb in the same position for a long time.
Vehicle sketch
[EDIT] See further down for more up-to-date version which uses Manchester encoding rather than async serial.
// Dagu 5 Chassis example.
// Author: Nick Gammon
// Date: 20th January 2012
#include <WProgram.h>
#include "RS485_protocol.h"
const unsigned long TIME_BEFORE_WE_GIVE_UP = 1000; // ms
const int WIRELESS_BAUD_RATE = 600;
const byte FWD = 0;
const byte BACKWD = 1;
const float LOW_BATTERY_VOLTAGE = 7.5;
// analog pins
const byte CURRENTA = 0; // A0
const byte CURRENTB = 1; // A1
const byte BATTERY_CHECK = 2; // A2
// digital pins
const byte LEFT_DIRECTION = 4;
const byte LEFT_MOTOR = 5;
const byte RIGHT_DIRECTION = 7;
const byte RIGHT_MOTOR = 6;
const byte RED_LED = 8;
#define CURRENT_LIMIT (1024 / 5) * 2.6 // amps
#define TIME_FORWARDS 10000
#define TIME_BACKWARDS 10000
#define TIME_TURN 1000
#define FULL_SPEED 255
#define SPEED_ADJUST 5
void jog (const byte direction)
{
digitalWrite (LEFT_DIRECTION, direction);
digitalWrite (RIGHT_DIRECTION, direction);
analogWrite (LEFT_MOTOR, 128);
analogWrite (RIGHT_MOTOR, 128);
delay (50);
analogWrite (LEFT_MOTOR, 0);
analogWrite (RIGHT_MOTOR, 0);
delay (50);
} // end of jog
// callback routines
void fWrite (const byte what)
{
Serial.print (what);
} // end of fWrite
int fAvailable ()
{
return Serial.available ();
} // end of fAvailable
int fRead ()
{
return Serial.read ();
} // end of fRead
void setup ()
{
analogWrite (LEFT_MOTOR, 0);
analogWrite (RIGHT_MOTOR, 0);
pinMode (LEFT_DIRECTION, OUTPUT);
pinMode (RIGHT_DIRECTION, OUTPUT);
Serial.begin (WIRELESS_BAUD_RATE);
// jog wheels to show we are ready ...
jog (FWD);
jog (BACKWD);
// low battery warning LED
pinMode (RED_LED, OUTPUT);
} // end of setup
// ---------------- MAIN LOOP -------------------
unsigned long lastCommand;
void loop ()
{
int voltage = analogRead (BATTERY_CHECK);
// 1024 would be 5V on the pin
// we have a voltage divider so the read voltage is half the battery
float volts = voltage / 1024.0 * 5.0 * 2.0;
if (volts < LOW_BATTERY_VOLTAGE)
{
analogWrite (LEFT_MOTOR, 0); // stop motors
analogWrite (RIGHT_MOTOR, 0);
digitalWrite (RED_LED, HIGH); // flash LED as warning
delay (50);
digitalWrite (RED_LED, LOW);
delay (950);
return; // don't try to use motors now
}
byte msg [2];
if (!recvMsg (fAvailable, fRead, msg, sizeof (msg)))
{
// no command for a second? stop!!!
if (millis () - lastCommand >= TIME_BEFORE_WE_GIVE_UP)
{
analogWrite (LEFT_MOTOR, 0); // stop motors
analogWrite (RIGHT_MOTOR, 0);
}
return;
}
lastCommand = millis ();
byte joy_x_axis = msg [0];
byte joy_y_axis = msg [1];
// experimentation shows the Nunchuk goes from about 30 to 230, with around 130 in the middle
int X = map (joy_x_axis, 31, 230, -100, 100);
int Y = map (joy_y_axis, 28, 222, -100, 100);
// ignore slightly off center as that just makes the motor whine
if (abs (X) < 5)
X = 0;
if (abs (Y) < 5)
Y = 0;
// find hypotenuse
float hyp = sqrt (X * X + Y * Y);
// find the sine of the angle
float s = (float) X / hyp;
// find the angle
float angle = asin (s) * 100.0;
// we want 100 to be 255 on the motor
int lspeed = hyp * 2.5;
int rspeed = hyp * 2.5;
if (Y > abs (X)) // forwards
{
digitalWrite (LEFT_DIRECTION, FWD);
digitalWrite (RIGHT_DIRECTION, FWD);
}
else if (-Y > abs (X)) // backwards
{
digitalWrite (LEFT_DIRECTION, BACKWD);
digitalWrite (RIGHT_DIRECTION, BACKWD);
}
else if (X >= abs (Y)) // right
{
digitalWrite (LEFT_DIRECTION, FWD);
digitalWrite (RIGHT_DIRECTION, BACKWD);
}
else if (-X >= abs (Y)) // left
{
digitalWrite (LEFT_DIRECTION, BACKWD);
digitalWrite (RIGHT_DIRECTION, FWD);
}
// fiddle around making turning better
if (Y > 0)
{
if (X > 0)
{
if (angle <= 80)
rspeed -= angle * (rspeed * 1.1 / 100);
else
rspeed = (angle - 80) * (rspeed * 1.1 / 100);
}
else if (X < 0)
{
if (abs (angle) <= 80)
lspeed -= abs (angle) * (lspeed * 1.1 / 100);
else
lspeed = (abs (angle) - 80) * (lspeed * 1.1 / 100);
}
} // end Y > 0
else
// Y is negative or zero
{
if (X > 0)
{
if (angle <= 80)
lspeed -= angle * (lspeed * 1.1 / 100);
else
lspeed = (angle - 80) * (lspeed * 1.1 / 100);
}
else if (X < 0)
{
if (abs (angle) <= 80)
rspeed -= abs (angle) * (rspeed * 1.1 / 100);
else
rspeed = (abs (angle) - 80) * (rspeed * 1.1 / 100);
}
} // end Y <= 0
// set motor speed, no less than 0 and no greater than 255
analogWrite (LEFT_MOTOR, max (0, min (255, lspeed)));
analogWrite (RIGHT_MOTOR, max (0, min (255, rspeed)));
} // end of loop
|