RCChannelsL293D by D Banks is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
The following post builds on previous posts relating to interfacing RC Equipment with micro controller projects.
Previous Related Posts
The basics -
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
Reading more channels -
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
Simple robot running to code presented later in this post -
http://rcarduino.blogspot.com/2012/05/rc-arduino-robot.html
The basics -
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
Reading more channels -
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
Simple robot running to code presented later in this post -
http://rcarduino.blogspot.com/2012/05/rc-arduino-robot.html
The post covers Arduino calibration with RC Equipment and the transformation of RC Steering and throttle channels into the pin logic levels and left and right PWM signals required to provide full proportional control of a tracked vehicle using the common L293D motor driver.
Why Calibration ?
In the very fist post dealing with RC Equipment we discussed how an RC Channel Signal is made up of a train of around 50 pulses per second and that each pulse is between 1000 and 2000 microseconds in length with neutral being 1500 microseconds.
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
In air applications the neutral point for the throttle channel is generally 1000 as braking and reverse are not available and so the full 1000 to 2000 microsecond range is used for forwards throttle control.
Many projects are based on the assumption that these values always apply, however this is not the case.
A variety of transmitter construction and environmental factors mean that we can rarely rely on neutral generating a pulse of exactly 1500 microseconds, nor should we expect full throttle, full braking or left and right end points to be exactly 1000 and 2000 microseconds. As an example, the values my transmitter provided by default are shown below -
Example minimum, center and maximum channel values from a Spectrum DX3S Transmitter/Receiver
Channel Value | Actual Pulse Width | Percentage of Assumed Range |
Full Left Steering | 1080 | 84.0 |
Full Right Steering | 1900 | 80.0 |
Center Steering | 1484 | 3.2 |
Full Brakes | 1108 | 78.4 |
Full Throttle | 1916 | 83.2 |
Neutral Throttle | 1524 | 4.8 |
Looking at the final percentage column we can see that the transmitter is sending only 80% of the expected range, this means our projects can only deliver 80% of their full performance.
A similar problem occurs with the throttle center point, the throttle channel is sending 1524 as the neutral point which corresponds to almost 5% throttle, this wastes power and causes unnecessary heat build up.
In order to overcome these problems I have built a simple 'one touch' calibration into the RCChannelsToL293 project.
One Touch Calibration
When the user presses the program button, the project enters PROGRAM_MODE for 10 seconds. During this time, the project examines all incoming signals to determine the highest and lowest value received.
All that is required is for the user to briefly apply full throttle and reverse then left and right steering for the code to record the end points.
The center points are captured immediately that program mode is entered, so it is important that the transmitter is in a neutral position when the program button is initially pressed.
Permanently Storing The Calibration
The project takes advantage of the ATMega microcontrollers onboard EEPROM memory to permanently store the end points. The user need only repeat the programing procedure if the transmitter/receiver is changed in the future.
The EEPROM (Electrically Erasable Programmable Read Only Memory) is a separate memory on the ATMega micro controller that can be used to store information between power cycles. To put it another way anything your store in EEPROM, is still going to be there when you reset the Arduino.
As we do not want to have to reprogram our RC Project every time we switch on, the EEPROM can be used to store settings and then read them in at power up.
The Code - Part 1 - Accessing the EEPROM
The Arduino provides a library for storing single byte values in EEPROM. To include the library in a project select Sketch/Import Library/EEPROM from the Arduino editor.
The library provides two functions EEPROM.read and EEPROM.write. These two functions work with a single byte at a time however the settings we want to store are held in two byte uint16_t variables. In order to read and write these variables we have to store and retrieve each byte separately.
// write a two byte channel setting to EEPROM
void writeChannelSetting(uint8_t nIndex,uint16_t unSetting)
{
EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}
{
EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}
// read a two byte channel setting from EEPROM
uint16_t readChannelSetting(uint8_t nIndex)
{
uint16_t unSetting = (EEPROM.read((nIndex*sizeof(uint16_t))+1)<<8);
unSetting += EEPROM.read(nIndex*sizeof(uint16_t));
return unSetting;
}
{
uint16_t unSetting = (EEPROM.read((nIndex*sizeof(uint16_t))+1)<<8);
unSetting += EEPROM.read(nIndex*sizeof(uint16_t));
return unSetting;
}
Part 2 - Implementing The Program Mode
To capture the settings we want to store I have added a program button, when this button is pushed the following steps are performed -
1) Set the mode variable gMode to PROGRAM_MODE
2) Set ulProgramModeExitTime to ten seconds from now
3) Set the throttle and steering channel center points to the current pulse width - for this reason it is important that all transmitter channels are at the neutral position when the program button is pressed.
4) Set all other end points to RC_NEUTRAL
5) Set gDirection to DIRECTION_STOP - this is used at a later point in the code to disable the L293D
if(false == digitalRead(PROGRAM_PIN))
{
// give 10 seconds to program
ulProgramModeExitTime = millis() + 10000;
gMode = MODE_PROGRAM;
{
// give 10 seconds to program
ulProgramModeExitTime = millis() + 10000;
gMode = MODE_PROGRAM;
unThrottleCenter = unThrottleIn;
unSteeringCenter = unSteeringIn;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
gDirection = DIRECTION_STOP;
delay(20);
}
unSteeringCenter = unSteeringIn;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
gDirection = DIRECTION_STOP;
delay(20);
}
The rest of the loop function implements two modes 1) MODE_RUN and 2) MODE_PROGRAM. In MODE_PROGRAM, we monitor the incoming throttle and steering signals and test them to see whether they represent new end points, if so we update the current end points to the new values. The end points are recorded in unThrottleMin, unThrottleMax, unSteeringMin and unSteeringMax.
We also check whether we have reached ulProgramModeExitTime if so we store the new end points in EEPROM and set gMode back to MODE_RUN.
if(gMode == MODE_PROGRAM)
{
if(ulProgramModeExitTime < millis())
{
// set to 0 and exit program mode
ulProgramModeExitTime = 0;
gMode = MODE_RUN;
}
if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
{
unThrottleMax = unThrottleIn;
}
else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
{
unThrottleMin = unThrottleIn;
}
if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
{
unSteeringMax = unSteeringIn;
}
else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
{
unSteeringMin = unSteeringIn;
}
}
{
if(ulProgramModeExitTime < millis())
{
// set to 0 and exit program mode
ulProgramModeExitTime = 0;
gMode = MODE_RUN;
}
if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
{
unThrottleMax = unThrottleIn;
}
else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
{
unThrottleMin = unThrottleIn;
}
if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
{
unSteeringMax = unSteeringIn;
}
else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
{
unSteeringMin = unSteeringIn;
}
}
Now that the calibration is covered we can move on to the fun part - controlling the motors.
Controlling Two Tracked Motors With An RC Transmitter.
In order to control a track vehicle with a standard RC Transmitter and receiver we need a way of converting from throttle and steering channels into two throttle channels and no steering channel.
In practice its very simple and can be outlined as follows -
1) Read the throttle channel and set the left and right motor speeds to the same value based on the level of input.
2) Read the steering channel, if the steering is outside of a central deadband, reduce the speed of the motor on the inside of the required direction in proportion to the level of steering input.
If the user turns the transmitter wheel 20% to the left, we will slow the left track down by 20% causing the truck to turn to the left.
It gets a little more complicated when we bring forwards and reverse into the mix and a little more complicated again when we introduce an ability to turn on the spot using counter rotation of the two motors when steering is applied at idle throttle.
In order to manage these additional requirements I have added direction and gear variables. The direction variable is initially set based on throttle input - FORWARDS or REVERSE. The gear variable is also throttle dependent, if the throttle is within the central dead band, the gear is set to IDLE, if it is outside the deadband, the gear is set to FULL.
Steering
The next section of code looks at the steering channel. If the gear has been set to IDLE and a steering angle is applied, the direction is overridden and set to ROTATE, this allows the truck to turn on the spot at idle. If the gear is FULL, the direction is not overridden, but the speed of the inside track is reduced in proportion to the level of steering.
In the case of 100% left or right steering input, the inside track will be stopped completely allowing the truck to pivot in a sharp turn around the inside track.
Rotation On The Spot At Idle -
// if at idle, set the direction to DIRECTION_ROTATE_RIGHT or DIRECTION_ROTATE_LEFT
// Speed of rotation is proportional to steering angle
case GEAR_IDLE:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_RIGHT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_LEFT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
}
break;
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_RIGHT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_LEFT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
}
break;
Proportional Steering During Forwards Or Reverse Motion
// if not at idle proportionally restrain inside track to turn vehicle around it
case GEAR_FULL:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
}
break;
case GEAR_FULL:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
}
break;
In the proportional steering mode, the map function is used to convert from steering input to a value determined by both the steering input and the throttle input. This is achieved in a single step by including the throttle input variable gThrottle in the output range passed to 'map'. This results in an output that is proportional to both the steering input unSteeringIn and the throttle input gThrottle.
In the next section of code, the L293 logic pins are set based on direction, this sets the required direction of rotation for each of the two motors.
At the end of this section analogWrite is used to set the individual PWM speeds or the two independent motors.
if((gDirection != gOldDirection) || (gGear != gOldGear))
{
gOldDirection = gDirection;
gOldGear = gGear;
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
switch(gDirection)
{
case DIRECTION_FORWARD:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_REVERSE:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_ROTATE_LEFT:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_ROTATE_RIGHT:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_STOP:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
break;
}
}
{
gOldDirection = gDirection;
gOldGear = gGear;
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
switch(gDirection)
{
case DIRECTION_FORWARD:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_REVERSE:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_ROTATE_LEFT:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_ROTATE_RIGHT:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_STOP:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
break;
}
}
The code presented provides us with full proportional forwards and reverse speed, left and right steering at any speed and in any direction and an extra bonus of on the spot rotation in either direction at idle.
The full code -
Enjoy
Duane B
#include <EEPROM.h>
// MultiChannel L293D
//
// rcarduino.blogspot.com
//
// A simple approach for reading two RC Channels from a hobby quality receiver
// and outputting to the common motor driver IC the L293D to drive a tracked vehicle
//
// We will use the Arduino to mix the channels to give car like steering using a standard two stick
// or pistol grip transmitter. The Aux channel will be used to switch and optional momentum mode on and off
//
// See related posts -
//
// Reading an RC Receiver - What does this signal look like and how do we read it -
// http://rcarduino.blogspot.co.uk/2012/01/how-to-read-rc-receiver-with.html
//
// The Arduino library only supports two interrupts, the Arduino pinChangeInt Library supports more than 20 -
// http://rcarduino.blogspot.co.uk/2012/03/need-more-interrupts-to-read-more.html
//
// The Arduino Servo Library supports upto 12 Servos on a single Arduino, read all about it here -
// http://rcarduino.blogspot.co.uk/2012/01/can-i-control-more-than-x-servos-with.html
//
// The wrong and then the right way to connect servos to Arduino
// http://rcarduino.blogspot.com/2012/04/servo-problems-with-arduino-part-1.html
// http://rcarduino.blogspot.com/2012/04/servo-problems-part-2-demonstration.html
//
// Using pinChangeInt library and Servo library to read three RC Channels and drive 3 RC outputs (mix of Servos and ESCs)
// http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
//
// rcarduino.blogspot.com
//
// if stopped and turn
// rotate on spot
// if crawling
// rotate on one side
// if forward or backward
// map
#define RC_NEUTRAL 1500
#define RC_MAX 2000
#define RC_MIN 1000
#define RC_DEADBAND 40
uint16_t unSteeringMin = RC_MIN;
uint16_t unSteeringMax = RC_MAX;
uint16_t unSteeringCenter = RC_NEUTRAL;
uint16_t unThrottleMin = RC_MIN;
uint16_t unThrottleMax = RC_MAX;
uint16_t unThrottleCenter = RC_NEUTRAL;
#define PWM_MIN 0
#define PWM_MAX 255
#define GEAR_NONE 1
#define GEAR_IDLE 1
#define GEAR_FULL 2
#define PWM_SPEED_LEFT 10
#define PWM_SPEED_RIGHT 11
#define LEFT1 5
#define LEFT2 6
#define RIGHT1 7
#define RIGHT2 8
#define PROGRAM_PIN 9
// Assign your channel in pins
#define THROTTLE_IN_PIN 2
#define STEERING_IN_PIN 3
// These bit flags are set in bUpdateFlagsShared to indicate which
// channels have new signals
#define THROTTLE_FLAG 1
#define STEERING_FLAG 2
// holds the update flags defined above
volatile uint8_t bUpdateFlagsShared;
// shared variables are updated by the ISR and read by loop.
// In loop we immediatley take local copies so that the ISR can keep ownership of the
// shared ones. To access these in loop
// we first turn interrupts off with noInterrupts
// we take a copy to use in loop and the turn interrupts back on
// as quickly as possible, this ensures that we are always able to receive new signals
volatile uint16_t unThrottleInShared;
volatile uint16_t unSteeringInShared;
// These are used to record the rising edge of a pulse in the calcInput functions
// They do not need to be volatile as they are only used in the ISR. If we wanted
// to refer to these in loop and the ISR then they would need to be declared volatile
uint32_t ulThrottleStart;
uint32_t ulSteeringStart;
uint8_t gThrottle = 0;
uint8_t gGear = GEAR_NONE;
uint8_t gOldGear = GEAR_NONE;
#define DIRECTION_STOP 0
#define DIRECTION_FORWARD 1
#define DIRECTION_REVERSE 2
#define DIRECTION_ROTATE_RIGHT 3
#define DIRECTION_ROTATE_LEFT 4
uint8_t gThrottleDirection = DIRECTION_STOP;
uint8_t gDirection = DIRECTION_STOP;
uint8_t gOldDirection = DIRECTION_STOP;
#define IDLE_MAX 80
#define MODE_RUN 0
#define MODE_PROGRAM 1
uint8_t gMode = MODE_RUN;
uint32_t ulProgramModeExitTime = 0;
// Index into the EEPROM Storage assuming a 0 based array of uint16_t
// Data to be stored low byte, high byte
#define EEPROM_INDEX_STEERING_MIN 0
#define EEPROM_INDEX_STEERING_MAX 1
#define EEPROM_INDEX_STEERING_CENTER 2
#define EEPROM_INDEX_THROTTLE_MIN 3
#define EEPROM_INDEX_THROTTLE_MAX 4
#define EEPROM_INDEX_THROTTLE_CENTER 5
void setup()
{
Serial.begin(9600);
Serial.println("RCChannelsTo293");
attachInterrupt(0 /* INT0 = THROTTLE_IN_PIN */,calcThrottle,CHANGE);
attachInterrupt(1 /* INT1 = STEERING_IN_PIN */,calcSteering,CHANGE);
pinMode(PWM_SPEED_LEFT,OUTPUT);
pinMode(PWM_SPEED_RIGHT,OUTPUT);
pinMode(LEFT1,OUTPUT);
pinMode(LEFT2,OUTPUT);
pinMode(RIGHT1,OUTPUT);
pinMode(RIGHT2,OUTPUT);
pinMode(PROGRAM_PIN,INPUT);
readSettingsFromEEPROM();
}
void loop()
{
// create local variables to hold a local copies of the channel inputs
// these are declared static so that thier values will be retained
// between calls to loop.
static uint16_t unThrottleIn;
static uint16_t unSteeringIn;
// local copy of update flags
static uint8_t bUpdateFlags;
// check shared update flags to see if any channels have a new signal
if(bUpdateFlagsShared)
{
noInterrupts(); // turn interrupts off quickly while we take local copies of the shared variables
// take a local copy of which channels were updated in case we need to use this in the rest of loop
bUpdateFlags = bUpdateFlagsShared;
// in the current code, the shared values are always populated
// so we could copy them without testing the flags
// however in the future this could change, so lets
// only copy when the flags tell us we can.
if(bUpdateFlags & THROTTLE_FLAG)
{
unThrottleIn = unThrottleInShared;
}
if(bUpdateFlags & STEERING_FLAG)
{
unSteeringIn = unSteeringInShared;
}
// clear shared copy of updated flags as we have already taken the updates
// we still have a local copy if we need to use it in bUpdateFlags
bUpdateFlagsShared = 0;
interrupts(); // we have local copies of the inputs, so now we can turn interrupts back on
// as soon as interrupts are back on, we can no longer use the shared copies, the interrupt
// service routines own these and could update them at any time. During the update, the
// shared copies may contain junk. Luckily we have our local copies to work with :-)
}
if(false == digitalRead(PROGRAM_PIN))
{
// give 10 seconds to program
ulProgramModeExitTime = millis() + 10000;
gMode = MODE_PROGRAM;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
unSteeringMin = RC_NEUTRAL;
unSteeringMax = RC_NEUTRAL;
unThrottleCenter = unThrottleIn;
unSteeringCenter = unSteeringIn;
gDirection = DIRECTION_STOP;
delay(20);
}
if(gMode == MODE_PROGRAM)
{
if(ulProgramModeExitTime < millis())
{
// set to 0 to exit program mode
ulProgramModeExitTime = 0;
gMode = MODE_RUN;
writeSettingsToEEPROM();
}
else
{
if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
{
unThrottleMax = unThrottleIn;
}
else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
{
unThrottleMin = unThrottleIn;
}
if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
{
unSteeringMax = unSteeringIn;
}
else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
{
unSteeringMin = unSteeringIn;
}
}
}
// do any processing from here onwards
// only use the local values unAuxIn, unThrottleIn and unSteeringIn, the shared
// variables unAuxInShared, unThrottleInShared, unSteeringInShared are always owned by
// the interrupt routines and should not be used in loop
if(gMode == MODE_RUN)
{
// we are checking to see if the channel value has changed, this is indicated
// by the flags. For the simple pass through we don't really need this check,
// but for a more complex project where a new signal requires significant processing
// this allows us to only calculate new values when we have new inputs, rather than
// on every cycle.
if(bUpdateFlags & THROTTLE_FLAG)
{
// A good idea would be to check the before and after value,
// if they are not equal we are receiving out of range signals
// this could be an error, interference or a transmitter setting change
// in any case its a good idea to at least flag it to the user somehow
unThrottleIn = constrain(unThrottleIn,unThrottleMin,unThrottleMax);
if(unThrottleIn > unThrottleCenter)
{
gThrottle = map(unThrottleIn,unThrottleCenter,unThrottleMax,PWM_MIN,PWM_MAX);
gThrottleDirection = DIRECTION_FORWARD;
}
else
{
gThrottle = map(unThrottleIn,unThrottleMin,unThrottleCenter,PWM_MAX,PWM_MIN);
gThrottleDirection = DIRECTION_REVERSE;
}
if(gThrottle < IDLE_MAX)
{
gGear = GEAR_IDLE;
}
else
{
gGear = GEAR_FULL;
}
}
if(bUpdateFlags & STEERING_FLAG)
{
uint8_t throttleLeft = gThrottle;
uint8_t throttleRight = gThrottle;
gDirection = gThrottleDirection;
// see previous comments regarding trapping out of range errors
// this is left for the user to decide how to handle and flag
unSteeringIn = constrain(unSteeringIn,unSteeringMin,unSteeringMax);
// if idle spin on spot
switch(gGear)
{
case GEAR_IDLE:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_RIGHT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_LEFT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
}
break;
// if not idle proportionally restrain inside track to turn vehicle around it
case GEAR_FULL:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
}
break;
}
analogWrite(PWM_SPEED_LEFT,throttleLeft);
analogWrite(PWM_SPEED_RIGHT,throttleRight);
}
}
if((gDirection != gOldDirection) || (gGear != gOldGear))
{
gOldDirection = gDirection;
gOldGear = gGear;
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
switch(gDirection)
{
case DIRECTION_FORWARD:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_REVERSE:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_ROTATE_LEFT:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_ROTATE_RIGHT:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_STOP:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
break;
}
}
bUpdateFlags = 0;
}
// simple interrupt service routine
void calcThrottle()
{
// if the pin is high, its a rising edge of the signal pulse, so lets record its value
if(digitalRead(THROTTLE_IN_PIN) == HIGH)
{
ulThrottleStart = micros();
}
else
{
// else it must be a falling edge, so lets get the time and subtract the time of the rising edge
// this gives use the time between the rising and falling edges i.e. the pulse duration.
unThrottleInShared = (uint16_t)(micros() - ulThrottleStart);
// use set the throttle flag to indicate that a new throttle signal has been received
bUpdateFlagsShared |= THROTTLE_FLAG;
}
}
void calcSteering()
{
if(digitalRead(STEERING_IN_PIN) == HIGH)
{
ulSteeringStart = micros();
}
else
{
unSteeringInShared = (uint16_t)(micros() - ulSteeringStart);
bUpdateFlagsShared |= STEERING_FLAG;
}
}
// Updated 04/06/2012 to use default values if no previous calibration is stored in EEPROM
void readSettingsFromEEPROM()
{
unSteeringMin = readChannelSetting(EEPROM_INDEX_STEERING_MIN);
if(unSteeringMin < RC_MIN || unSteeringMin > RC_NEUTRAL)
{
unSteeringMin = RC_MIN;
}
Serial.println(unSteeringMin);
unSteeringMax = readChannelSetting(EEPROM_INDEX_STEERING_MAX);
if(unSteeringMax > RC_MAX || unSteeringMax < RC_NEUTRAL)
{
unSteeringMax = RC_MAX;
}
Serial.println(unSteeringMax);
unSteeringCenter = readChannelSetting(EEPROM_INDEX_STEERING_CENTER);
if(unSteeringCenter < unSteeringMin || unSteeringCenter > unSteeringMax)
{
unSteeringCenter = RC_NEUTRAL;
}
Serial.println(unSteeringCenter);
unThrottleMin = readChannelSetting(EEPROM_INDEX_THROTTLE_MIN);
if(unThrottleMin < RC_MIN || unThrottleMin > RC_NEUTRAL)
{
unThrottleMin = RC_MIN;
}
Serial.println(unThrottleMin);
unThrottleMax = readChannelSetting(EEPROM_INDEX_THROTTLE_MAX);
if(unThrottleMax > RC_MAX || unThrottleMax < RC_NEUTRAL)
{
unThrottleMax = RC_MAX;
}
Serial.println(unThrottleMax);
unThrottleCenter = readChannelSetting(EEPROM_INDEX_THROTTLE_CENTER);
if(unThrottleCenter < unThrottleMin || unThrottleCenter > unThrottleMax)
{
unThrottleCenter = RC_NEUTRAL;
}
Serial.println(unThrottleCenter);
}
void writeSettingsToEEPROM()
{
writeChannelSetting(EEPROM_INDEX_STEERING_MIN,unSteeringMin);
writeChannelSetting(EEPROM_INDEX_STEERING_MAX,unSteeringMax);
writeChannelSetting(EEPROM_INDEX_STEERING_CENTER,unSteeringCenter);
writeChannelSetting(EEPROM_INDEX_THROTTLE_MIN,unThrottleMin);
writeChannelSetting(EEPROM_INDEX_THROTTLE_MAX,unThrottleMax);
writeChannelSetting(EEPROM_INDEX_THROTTLE_CENTER,unThrottleCenter);
Serial.println(unSteeringMin);
Serial.println(unSteeringMax);
Serial.println(unSteeringCenter);
Serial.println(unThrottleMin);
Serial.println(unThrottleMax);
Serial.println(unThrottleCenter);
}
uint16_t readChannelSetting(uint8_t nStart)
{
uint16_t unSetting = (EEPROM.read((nStart*sizeof(uint16_t))+1)<<8);
unSetting += EEPROM.read(nStart*sizeof(uint16_t));
return unSetting;
}
void writeChannelSetting(uint8_t nIndex,uint16_t unSetting)
{
EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}
// MultiChannel L293D
//
// rcarduino.blogspot.com
//
// A simple approach for reading two RC Channels from a hobby quality receiver
// and outputting to the common motor driver IC the L293D to drive a tracked vehicle
//
// We will use the Arduino to mix the channels to give car like steering using a standard two stick
// or pistol grip transmitter. The Aux channel will be used to switch and optional momentum mode on and off
//
// See related posts -
//
// Reading an RC Receiver - What does this signal look like and how do we read it -
// http://rcarduino.blogspot.co.uk/2012/01/how-to-read-rc-receiver-with.html
//
// The Arduino library only supports two interrupts, the Arduino pinChangeInt Library supports more than 20 -
// http://rcarduino.blogspot.co.uk/2012/03/need-more-interrupts-to-read-more.html
//
// The Arduino Servo Library supports upto 12 Servos on a single Arduino, read all about it here -
// http://rcarduino.blogspot.co.uk/2012/01/can-i-control-more-than-x-servos-with.html
//
// The wrong and then the right way to connect servos to Arduino
// http://rcarduino.blogspot.com/2012/04/servo-problems-with-arduino-part-1.html
// http://rcarduino.blogspot.com/2012/04/servo-problems-part-2-demonstration.html
//
// Using pinChangeInt library and Servo library to read three RC Channels and drive 3 RC outputs (mix of Servos and ESCs)
// http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
//
// rcarduino.blogspot.com
//
// if stopped and turn
// rotate on spot
// if crawling
// rotate on one side
// if forward or backward
// map
#define RC_NEUTRAL 1500
#define RC_MAX 2000
#define RC_MIN 1000
#define RC_DEADBAND 40
uint16_t unSteeringMin = RC_MIN;
uint16_t unSteeringMax = RC_MAX;
uint16_t unSteeringCenter = RC_NEUTRAL;
uint16_t unThrottleMin = RC_MIN;
uint16_t unThrottleMax = RC_MAX;
uint16_t unThrottleCenter = RC_NEUTRAL;
#define PWM_MIN 0
#define PWM_MAX 255
#define GEAR_NONE 1
#define GEAR_IDLE 1
#define GEAR_FULL 2
#define PWM_SPEED_LEFT 10
#define PWM_SPEED_RIGHT 11
#define LEFT1 5
#define LEFT2 6
#define RIGHT1 7
#define RIGHT2 8
#define PROGRAM_PIN 9
// Assign your channel in pins
#define THROTTLE_IN_PIN 2
#define STEERING_IN_PIN 3
// These bit flags are set in bUpdateFlagsShared to indicate which
// channels have new signals
#define THROTTLE_FLAG 1
#define STEERING_FLAG 2
// holds the update flags defined above
volatile uint8_t bUpdateFlagsShared;
// shared variables are updated by the ISR and read by loop.
// In loop we immediatley take local copies so that the ISR can keep ownership of the
// shared ones. To access these in loop
// we first turn interrupts off with noInterrupts
// we take a copy to use in loop and the turn interrupts back on
// as quickly as possible, this ensures that we are always able to receive new signals
volatile uint16_t unThrottleInShared;
volatile uint16_t unSteeringInShared;
// These are used to record the rising edge of a pulse in the calcInput functions
// They do not need to be volatile as they are only used in the ISR. If we wanted
// to refer to these in loop and the ISR then they would need to be declared volatile
uint32_t ulThrottleStart;
uint32_t ulSteeringStart;
uint8_t gThrottle = 0;
uint8_t gGear = GEAR_NONE;
uint8_t gOldGear = GEAR_NONE;
#define DIRECTION_STOP 0
#define DIRECTION_FORWARD 1
#define DIRECTION_REVERSE 2
#define DIRECTION_ROTATE_RIGHT 3
#define DIRECTION_ROTATE_LEFT 4
uint8_t gThrottleDirection = DIRECTION_STOP;
uint8_t gDirection = DIRECTION_STOP;
uint8_t gOldDirection = DIRECTION_STOP;
#define IDLE_MAX 80
#define MODE_RUN 0
#define MODE_PROGRAM 1
uint8_t gMode = MODE_RUN;
uint32_t ulProgramModeExitTime = 0;
// Index into the EEPROM Storage assuming a 0 based array of uint16_t
// Data to be stored low byte, high byte
#define EEPROM_INDEX_STEERING_MIN 0
#define EEPROM_INDEX_STEERING_MAX 1
#define EEPROM_INDEX_STEERING_CENTER 2
#define EEPROM_INDEX_THROTTLE_MIN 3
#define EEPROM_INDEX_THROTTLE_MAX 4
#define EEPROM_INDEX_THROTTLE_CENTER 5
void setup()
{
Serial.begin(9600);
Serial.println("RCChannelsTo293");
attachInterrupt(0 /* INT0 = THROTTLE_IN_PIN */,calcThrottle,CHANGE);
attachInterrupt(1 /* INT1 = STEERING_IN_PIN */,calcSteering,CHANGE);
pinMode(PWM_SPEED_LEFT,OUTPUT);
pinMode(PWM_SPEED_RIGHT,OUTPUT);
pinMode(LEFT1,OUTPUT);
pinMode(LEFT2,OUTPUT);
pinMode(RIGHT1,OUTPUT);
pinMode(RIGHT2,OUTPUT);
pinMode(PROGRAM_PIN,INPUT);
readSettingsFromEEPROM();
}
void loop()
{
// create local variables to hold a local copies of the channel inputs
// these are declared static so that thier values will be retained
// between calls to loop.
static uint16_t unThrottleIn;
static uint16_t unSteeringIn;
// local copy of update flags
static uint8_t bUpdateFlags;
// check shared update flags to see if any channels have a new signal
if(bUpdateFlagsShared)
{
noInterrupts(); // turn interrupts off quickly while we take local copies of the shared variables
// take a local copy of which channels were updated in case we need to use this in the rest of loop
bUpdateFlags = bUpdateFlagsShared;
// in the current code, the shared values are always populated
// so we could copy them without testing the flags
// however in the future this could change, so lets
// only copy when the flags tell us we can.
if(bUpdateFlags & THROTTLE_FLAG)
{
unThrottleIn = unThrottleInShared;
}
if(bUpdateFlags & STEERING_FLAG)
{
unSteeringIn = unSteeringInShared;
}
// clear shared copy of updated flags as we have already taken the updates
// we still have a local copy if we need to use it in bUpdateFlags
bUpdateFlagsShared = 0;
interrupts(); // we have local copies of the inputs, so now we can turn interrupts back on
// as soon as interrupts are back on, we can no longer use the shared copies, the interrupt
// service routines own these and could update them at any time. During the update, the
// shared copies may contain junk. Luckily we have our local copies to work with :-)
}
if(false == digitalRead(PROGRAM_PIN))
{
// give 10 seconds to program
ulProgramModeExitTime = millis() + 10000;
gMode = MODE_PROGRAM;
unThrottleMin = RC_NEUTRAL;
unThrottleMax = RC_NEUTRAL;
unSteeringMin = RC_NEUTRAL;
unSteeringMax = RC_NEUTRAL;
unThrottleCenter = unThrottleIn;
unSteeringCenter = unSteeringIn;
gDirection = DIRECTION_STOP;
delay(20);
}
if(gMode == MODE_PROGRAM)
{
if(ulProgramModeExitTime < millis())
{
// set to 0 to exit program mode
ulProgramModeExitTime = 0;
gMode = MODE_RUN;
writeSettingsToEEPROM();
}
else
{
if(unThrottleIn > unThrottleMax && unThrottleIn <= RC_MAX)
{
unThrottleMax = unThrottleIn;
}
else if(unThrottleIn < unThrottleMin && unThrottleIn >= RC_MIN)
{
unThrottleMin = unThrottleIn;
}
if(unSteeringIn > unSteeringMax && unSteeringIn <= RC_MAX)
{
unSteeringMax = unSteeringIn;
}
else if(unSteeringIn < unSteeringMin && unSteeringIn >= RC_MIN)
{
unSteeringMin = unSteeringIn;
}
}
}
// do any processing from here onwards
// only use the local values unAuxIn, unThrottleIn and unSteeringIn, the shared
// variables unAuxInShared, unThrottleInShared, unSteeringInShared are always owned by
// the interrupt routines and should not be used in loop
if(gMode == MODE_RUN)
{
// we are checking to see if the channel value has changed, this is indicated
// by the flags. For the simple pass through we don't really need this check,
// but for a more complex project where a new signal requires significant processing
// this allows us to only calculate new values when we have new inputs, rather than
// on every cycle.
if(bUpdateFlags & THROTTLE_FLAG)
{
// A good idea would be to check the before and after value,
// if they are not equal we are receiving out of range signals
// this could be an error, interference or a transmitter setting change
// in any case its a good idea to at least flag it to the user somehow
unThrottleIn = constrain(unThrottleIn,unThrottleMin,unThrottleMax);
if(unThrottleIn > unThrottleCenter)
{
gThrottle = map(unThrottleIn,unThrottleCenter,unThrottleMax,PWM_MIN,PWM_MAX);
gThrottleDirection = DIRECTION_FORWARD;
}
else
{
gThrottle = map(unThrottleIn,unThrottleMin,unThrottleCenter,PWM_MAX,PWM_MIN);
gThrottleDirection = DIRECTION_REVERSE;
}
if(gThrottle < IDLE_MAX)
{
gGear = GEAR_IDLE;
}
else
{
gGear = GEAR_FULL;
}
}
if(bUpdateFlags & STEERING_FLAG)
{
uint8_t throttleLeft = gThrottle;
uint8_t throttleRight = gThrottle;
gDirection = gThrottleDirection;
// see previous comments regarding trapping out of range errors
// this is left for the user to decide how to handle and flag
unSteeringIn = constrain(unSteeringIn,unSteeringMin,unSteeringMax);
// if idle spin on spot
switch(gGear)
{
case GEAR_IDLE:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_RIGHT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringCenter,unSteeringMax,PWM_MIN,PWM_MAX);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
gDirection = DIRECTION_ROTATE_LEFT;
// use steering to set throttle
throttleRight = throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MAX,PWM_MIN);
}
break;
// if not idle proportionally restrain inside track to turn vehicle around it
case GEAR_FULL:
if(unSteeringIn > (unSteeringCenter + RC_DEADBAND))
{
throttleRight = map(unSteeringIn,unSteeringCenter,unSteeringMax,gThrottle,PWM_MIN);
}
else if(unSteeringIn < (unSteeringCenter - RC_DEADBAND))
{
throttleLeft = map(unSteeringIn,unSteeringMin,unSteeringCenter,PWM_MIN,gThrottle);
}
break;
}
analogWrite(PWM_SPEED_LEFT,throttleLeft);
analogWrite(PWM_SPEED_RIGHT,throttleRight);
}
}
if((gDirection != gOldDirection) || (gGear != gOldGear))
{
gOldDirection = gDirection;
gOldGear = gGear;
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
switch(gDirection)
{
case DIRECTION_FORWARD:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_REVERSE:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_ROTATE_LEFT:
digitalWrite(LEFT1,HIGH);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,HIGH);
break;
case DIRECTION_ROTATE_RIGHT:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,HIGH);
digitalWrite(RIGHT1,HIGH);
digitalWrite(RIGHT2,LOW);
break;
case DIRECTION_STOP:
digitalWrite(LEFT1,LOW);
digitalWrite(LEFT2,LOW);
digitalWrite(RIGHT1,LOW);
digitalWrite(RIGHT2,LOW);
break;
}
}
bUpdateFlags = 0;
}
// simple interrupt service routine
void calcThrottle()
{
// if the pin is high, its a rising edge of the signal pulse, so lets record its value
if(digitalRead(THROTTLE_IN_PIN) == HIGH)
{
ulThrottleStart = micros();
}
else
{
// else it must be a falling edge, so lets get the time and subtract the time of the rising edge
// this gives use the time between the rising and falling edges i.e. the pulse duration.
unThrottleInShared = (uint16_t)(micros() - ulThrottleStart);
// use set the throttle flag to indicate that a new throttle signal has been received
bUpdateFlagsShared |= THROTTLE_FLAG;
}
}
void calcSteering()
{
if(digitalRead(STEERING_IN_PIN) == HIGH)
{
ulSteeringStart = micros();
}
else
{
unSteeringInShared = (uint16_t)(micros() - ulSteeringStart);
bUpdateFlagsShared |= STEERING_FLAG;
}
}
// Updated 04/06/2012 to use default values if no previous calibration is stored in EEPROM
void readSettingsFromEEPROM()
{
unSteeringMin = readChannelSetting(EEPROM_INDEX_STEERING_MIN);
if(unSteeringMin < RC_MIN || unSteeringMin > RC_NEUTRAL)
{
unSteeringMin = RC_MIN;
}
Serial.println(unSteeringMin);
unSteeringMax = readChannelSetting(EEPROM_INDEX_STEERING_MAX);
if(unSteeringMax > RC_MAX || unSteeringMax < RC_NEUTRAL)
{
unSteeringMax = RC_MAX;
}
Serial.println(unSteeringMax);
unSteeringCenter = readChannelSetting(EEPROM_INDEX_STEERING_CENTER);
if(unSteeringCenter < unSteeringMin || unSteeringCenter > unSteeringMax)
{
unSteeringCenter = RC_NEUTRAL;
}
Serial.println(unSteeringCenter);
unThrottleMin = readChannelSetting(EEPROM_INDEX_THROTTLE_MIN);
if(unThrottleMin < RC_MIN || unThrottleMin > RC_NEUTRAL)
{
unThrottleMin = RC_MIN;
}
Serial.println(unThrottleMin);
unThrottleMax = readChannelSetting(EEPROM_INDEX_THROTTLE_MAX);
if(unThrottleMax > RC_MAX || unThrottleMax < RC_NEUTRAL)
{
unThrottleMax = RC_MAX;
}
Serial.println(unThrottleMax);
unThrottleCenter = readChannelSetting(EEPROM_INDEX_THROTTLE_CENTER);
if(unThrottleCenter < unThrottleMin || unThrottleCenter > unThrottleMax)
{
unThrottleCenter = RC_NEUTRAL;
}
Serial.println(unThrottleCenter);
}
void writeSettingsToEEPROM()
{
writeChannelSetting(EEPROM_INDEX_STEERING_MIN,unSteeringMin);
writeChannelSetting(EEPROM_INDEX_STEERING_MAX,unSteeringMax);
writeChannelSetting(EEPROM_INDEX_STEERING_CENTER,unSteeringCenter);
writeChannelSetting(EEPROM_INDEX_THROTTLE_MIN,unThrottleMin);
writeChannelSetting(EEPROM_INDEX_THROTTLE_MAX,unThrottleMax);
writeChannelSetting(EEPROM_INDEX_THROTTLE_CENTER,unThrottleCenter);
Serial.println(unSteeringMin);
Serial.println(unSteeringMax);
Serial.println(unSteeringCenter);
Serial.println(unThrottleMin);
Serial.println(unThrottleMax);
Serial.println(unThrottleCenter);
}
uint16_t readChannelSetting(uint8_t nStart)
{
uint16_t unSetting = (EEPROM.read((nStart*sizeof(uint16_t))+1)<<8);
unSetting += EEPROM.read(nStart*sizeof(uint16_t));
return unSetting;
}
void writeChannelSetting(uint8_t nIndex,uint16_t unSetting)
{
EEPROM.write(nIndex*sizeof(uint16_t),lowByte(unSetting));
EEPROM.write((nIndex*sizeof(uint16_t))+1,highByte(unSetting));
}
Thank you for this post! That really is a great project.
ReplyDeleteWhat I'm also very interested in is to generate a PPM signal using an Arduino. I would love to use a Xbox controller or joystick at my laptop to control a rc plane. My dream is to connect the arduino through usb to my laptop and then a rc transmitter to the arduino.
Somebody already did something similar and is now selling it:
http://diydrones.com/profiles/blogs/usbtoppm-generator-for-pc
http://www.flytron.com/56-compufly-usbtoppm-converter.html
Another project here:
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1193927390
Another one:
http://fpv-community.com/forum/showthread.php?tid=345
An approach without Arduino. The generated PPM signal is outputted as audio through the headphone jack:
https://www.insecure.ws/2010/03/09/control-rc-aircrafts-from-your-computer-for-0
You can simply plug in an audio cable to the headphone jack of the pc and connect it to the student/teacher jack of a DX7 remote.
What I would like to test is to directly send the PPM signal to a rc transmitter (e.g. Frsky) without using a rc remote.
Do you think this is possible?
This one is even better:
ReplyDeletehttp://www.nghobbies.com/cart/index.php?main_page=product_info&cPath=1_10_135&products_id=486
I tested the code on the Arduino MEGA removing the part of the EEPROM Recording and using only one channel of the L293D. The problem is that the engine takes longer than 3 seconds to respond to the command by radio control if I increase the baud rate of serial port above 9600. What's going on?
ReplyDeleteHi Celso, Your comment suggests that you have the code working well with a good response speed, but have a problem if you change the serial speed, can you confirm this is correct ?
ReplyDeleteDuane B
Yes, this it. You know what is? You want see the code? Sorry my english.
DeleteThis comment has been removed by the author.
ReplyDeleteSo what is the th and steering flags ? sorry I am a bit noob still :D
ReplyDeleteThe throttle and steering flags are explained in these two posts
ReplyDeleteBasic background -
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
and in this one why we need the flags -
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html
Duane B
What do I connect to throttle and steering flags, because I cant get this to work?
ReplyDeleteThe code here is based on the example code in this link -
ReplyDeletehttp://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
Try to get this working with your set up first. You will need your receiver ground connected to the Arduino ground and the signal (white) wire connected to Arduino pin 2. You will also need to power your receiver, 4 AA Batteries will do or the BEC output of an Electronic Speed Controller if you have one.
Duane B
Hey, I have some questions I am hoping you could answer.
ReplyDelete1)You mention that a button is needed to initiate the calibration process. Does this mean you need an lcd screen and a button to interface with the user?
2) I am planning to control 4 servo motors directly from Arduino. Would the code work without the use of a L293D driver?
1) No LCD required. The button is simply used to tell the code to listen to the incoming pulses for a few seconds. When pushing the button, the controls should be centered, the code will record this center point as part of the calibration. Next you should move your controls back and forth through thier range, the code will monitor and record the highest and lowest values it sees. Thats all thats needed for calibration.
ReplyDelete2) Yes, but your going to have to do some work on it. Servo control is very simple with the Arduino Servo library, have a look at the yaw control project which is very similar to this but drives a servo and an ESC.
http://rcarduino.blogspot.com/2012/07/rcarduino-yaw-control-part-2.html
Duane B
Duane, love the blog, thanks so much for publishing your work.
ReplyDeleteI'm a bit confused by the RCArduinoFastLib. I'm running an Adafruit Motor Shield on a Mega ADK to control a pair of 37D 12v motors with an off label pistol RC set. I think where I'm having my disconnect is that, since I'm not using servos, why am I attaching them, and the output pin has me a bit baffled as well. Is the CRCArduinoFastServos::attach() just a code object? Or am I missing something here, and should be using your code a bit differently, ie: as an ESC?
As I'm using the motor shield, I don't need your code to control anything, just spit out the readings from the receiver.
I'm still reading through all the bolgs/docs, but I could really use some guidance here.
regards, Richard
Hi,
ReplyDeleteAs you are not planning to use servos, you do not have the problem that rcarduinofastlib is designed to solve. I assume that your motor sheild uses analogWrite to control the motor speeds in which case, the code in this post is all you need.
Duane B
Hi Duane,
ReplyDeletenice one! this is ALMOST(!) exactly what I need at the moment. well its "almost" because im using LB1630 as my motor driver chip. has very low standby current compared to the L293D(24-60mA). only lacking pin/function for the old chip LB1630 is enable pin. but I don't worry too much about it. I could add a CD4066 switch(to act as enable function) or I could study more of your code and do some minor edits =) AND I operate my motors to only 3.6V nominal(Li-Ion battery)..
I tried compiling the above code to arduino 0022. no errors. So I guess its safe to assume I don't need "Special" drivers to use this code?
my current setup is 4 channel encoder->315MHz tx ----> 315MHz rx->4ch decoder->ch1&ch2(use this arduino) then ch3&ch4 is servo pan/tilt for camera(wireless 1.2GHz)
Best Regards,
Raffy
Hi Duane,
ReplyDeleteI am looking at building your mini tracked RC vehical and from comments on your other pages I belive that you said that the L293D is wired up from the data sheet. I get that part but I was wondering are you using the Enable pins to control the speed on both sides of the chip via PWM?
So the Arduino pins 10 & 11 are connected to the Enable pins 1 & 9. Then pins 2, 7, 10 and 15 on the L293D are controlling the direction of the motors.
This comes from looking at this part of the code:
#define PWM_SPEED_LEFT 10
#define PWM_SPEED_RIGHT 11
I have I got that correct or am I off a bit?
Regards,
James
Yep, worked that bit out. Yes 10 and 11 control speed via PWM to the enable pins.
DeleteHi, which Arduino board are you using? Or, will it work in UNO, Leonardo, DUEmilanove...? Thanks
ReplyDeleteCould I use this code in an Arduino Nano?
ReplyDeleteUNO
ReplyDeleteDuane.
Hi Duane, tks a lot for sharing your project!! I’m trying to build it, but I have a doubt regarding the calibration button.. Could you explain how it must be connected to the Arduino Uno board?
ReplyDeleteI would like to drive 2 x 15 amps motors, do you have a diagram to build a more powerful h-bridge driver? Is it possible to use solid state relay?
TKS again.
Steve
What if I want to use the sketch for controlling a single motor using just one channel? Is it enough to unplugging the steering channel or do I have to add a switch and alter the code to "always think" that the steering value is neutral? Can I put a 1500 ms (RC NEUTRAL) value in the steering pin?
ReplyDeleteHi duane.
ReplyDelete1-The Pin-2 and Pin-3 for signal input is PPM?
2-El Pin-9 program are PULL-UP or PULL-DOW?
Your tutorial these very good ..... Cheers!
// This is the main code, it should run on the main processor.
ReplyDelete// Read PPM signals from 2 channels of an RC reciever and convert the values to PWM in either direction.
// digital pins 5 & 6 control motor1, digital pins 7 & 8 control motor2.
// DP 10, 11, 12 and 13 are the enable pins, previouly they were indicator leds
// DP 2 and 3 are inputs from the R/C receiver.
// All analog pins are open.
// Motor 1
const int motor1_a = 5;
const int motor1_b = 6;
// Motor 2
const int motor2_a = 7;
const int motor2_b = 8;
// Enables
const int ledPin1 = 10;
const int ledPin2 = 11;
const int ledPin3 = 12;
const int ledPin4 = 13;
// Inputs from RC receiver
const int servo1 = 2;
const int servo2 = 3;
// Variables:
// Servo 1
volatile unsigned long servo1_startPulse;
volatile unsigned servo1_val;
volatile int adj_val1;
volatile int servo1_Ready;
//Servo 2
volatile unsigned long servo2_startPulse;
volatile unsigned servo2_val;
volatile int adj_val2;
volatile int servo2_Ready;
//Deadbands
int deadband_high = 255;
int deadband_low = 220;
//pwm Limits
int pwm_ceiling = 256;
int pwm_floor = 255;
//Max servo values
int low1 = 1000;
int high1 = 2000;
int low2 = 1000;
int high2 = 1900;
int n = 0;
void setup() {
Serial.begin(9600);
//motor1 pins
pinMode(motor1_a, OUTPUT);
pinMode(motor1_b, OUTPUT);
//motor2 pins
pinMode(motor2_a, OUTPUT);
pinMode(motor2_b, OUTPUT);
//led's / Enables
pinMode(ledPin1, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(ledPin3, OUTPUT);
pinMode(ledPin4, OUTPUT);
//PPM inputs from RC receiver
pinMode(servo1, INPUT); //Pin 2 as input
pinMode(servo2, INPUT); //Pin 3 as input
//write enables low at startup
digitalWrite(ledPin1, LOW);
digitalWrite(ledPin2, LOW);
digitalWrite(ledPin3, LOW);
digitalWrite(ledPin4, LOW);
delay(1200);
attachInterrupt(0, rc1_begin, RISING); // catch interrupt 0 (digital pin 2) going HIGH and send to rc1()
attachInterrupt(1, rc2_begin, RISING); // catch interrupt 1 (digital pin 3) going HIGH and send to rc2()
}
////////// attach servo signal interrupts to catch signals as they go HIGH then again as they go LOW, then calculate the pulse length.
void rc1_begin() { // enter rc1_begin when interrupt pin goes HIGH.
servo1_startPulse = micros(); // record microseconds() value as servo1_startPulse
detachInterrupt(0); // after recording the value, detach the interrupt from rc1_begin
attachInterrupt(0, rc1_end, FALLING); // re-attach the interrupt as rc1_end, so we can record the value when it goes low
}
void rc1_end() {
servo1_val = micros() - servo1_startPulse; // when interrupt pin goes LOW, record the total pulse length by subtracting previous start value from current micros() vlaue.
detachInterrupt(0); // detach and get ready to go HIGH again
attachInterrupt(0, rc1_begin, RISING);
}
void rc2_begin() {
servo2_startPulse = micros();
detachInterrupt(1);
attachInterrupt(1, rc2_end, FALLING);
}
void rc2_end() {
servo2_val = micros() - servo2_startPulse;
detachInterrupt(1);
attachInterrupt(1, rc2_begin, RISING);
}
/////// servo interrupts end
/////// MAIN LOOP
void loop() {
///// channel 1 good signal check
if (servo1_val < 600 || servo1_val > 2400) { // only set the servo1_Ready flag if the value is a valid Servo pulse between 600-2400 microseconds.
servo1_Ready = false;
servo1_val = 1500;
}
else {
servo1_Ready = true; // if not, don't pass the value to be processed
}
///// channel 2 good signal check
if (servo2_val < 600 || servo2_val > 2400) {
servo2_Ready = false;
servo2_val = 1500;
}
else {
servo2_Ready = true;
}
////////// channel 1 motor update
ReplyDeleteif (servo1_Ready) {
servo1_Ready = false;
adj_val1 = map(constrain(servo1_val, 600, 2400), low1, high1, 0, 511);
constrain(adj_val1, 0, 511);
if (adj_val1 < 0) {
adj_val1 = 0;
}
if (adj_val1 > 511) {
adj_val1 = 511;
}
if (adj_val1 > deadband_high) {
//Forward
digitalWrite(motor1_b, LOW);
analogWrite(motor1_a, adj_val1 - pwm_ceiling);
digitalWrite(ledPin1, HIGH);
}
else if (adj_val1 < deadband_low) {
//REVERSE
digitalWrite(motor1_a, LOW);
analogWrite(motor1_b, pwm_floor - adj_val1);
digitalWrite(ledPin2, HIGH);
}
else {
//STOP
digitalWrite(motor1_a, LOW);
digitalWrite(motor1_b, LOW);
}
}
///////////// channel 2 motor update
if (servo2_Ready) {
servo2_Ready = false;
adj_val2 = map(constrain(servo2_val, 600, 2400), low1, high1, 0, 511);
constrain(adj_val2, 0, 511);
if (adj_val2 < 0) {
adj_val2 = 0;
}
if (adj_val2 > 511) {
adj_val2 = 511;
}
if (adj_val2 > deadband_high) {
//Forward
digitalWrite(motor2_b, LOW);
analogWrite(motor2_a, adj_val2 - pwm_ceiling);
digitalWrite(ledPin3, HIGH);
}
else if (adj_val2 < deadband_low) {
//REVERSE
digitalWrite(motor2_a, LOW);
analogWrite(motor2_b, pwm_floor - adj_val2);
digitalWrite(ledPin4, HIGH);
}
else {
//STOP
digitalWrite(motor2_a, LOW);
digitalWrite(motor2_b, LOW);
}
}
//print values to Serial monitor in Arduino IDE
Serial.print("ch1: ");
Serial.print(adj_val1);
Serial.print(" ");
Serial.print("rc1: ");
Serial.print(servo1_val);
Serial.print(" ");
Serial.print("ch2: ");
Serial.print(adj_val2);
Serial.print(" ");
Serial.print("rc2: ");
Serial.print(servo2_val);
Serial.print(" ");
Serial.print("loop counter: ");
Serial.print(n);
Serial.println(" ");
}
the problem im having with this code is trying to auto calibrate it using EEPROM etc can someone please help as i am a noob
ReplyDeleteReading the comments at the start of the code you have posted, it looks like your trying to acheive the same as the working code already provided in the blog post. It also looks as if your project will go on to do something more interesting than just Convert RC PWM to analogWrite PWM, so why waste time on the RC bit if a working example is already available for you to use ? Duane B
ReplyDeleteDuane B, I can see your a legend just by looking at this blog and seeing how much time you've spend helping others.
DeleteMy code as sentimental value with the amount of time I've spend on it.
I am using a modified helicopter controller and using the 2 sticks for control of the wheels on my bot.
The problem I'm having is with the calibrating and saving the value to EEPROM.
I just cant work out how to implement your code to work with mine.
Please bud I'm begging.
Tom
sorry mate thought i better clarify, Mary cavanagh whalley is my partner, I was logged in under the wrong account
DeleteIve added this to the code but cant work out how to save it to EEPROM
Deletevoid setup() {
Serial.begin(9600);
// calibrate channel 1 (servo1 / Pin 2) during the first five seconds
// calibrate channel 2 (servo2 / Pin 3) during the first five seconds
digitalWrite(ledPin5, HIGH); // Turn on ledPin5 to show calibration process is active
while (millis() < 5000) {
servo1_val = analogRead(servo1);
// record the maximum servo 1 value
if (servo1_val > high1) {
high1 = servo1_val;
}
// record the minimum servo1 value
if (servo1_val < low1) {
low1 = servo1_val;
}
// record the maximum servo 2 value
if (servo2_val > high2) {
high2 = servo2_val;
}
// record the minimum servo 2 value
if (servo2_val < low2) {
low2 = servo2_val;
}
digitalWrite(ledPin5,LOW); // Turn off ledPin5 to show calibration has finished
}
///// MAIN LOOP
void loop() {
///// Apply the calibrated setting to the code
servo1_val = map (servo1_val, low1, high1, 1000, 2000);
servo2_val = map (servo2_val, low2, high2, 1000, 2000);
///// Constrain the calibrated setting to 1000 and 2000
servo1_val = constrain (servo1_val, 1000, 2000);
servo2_val = constrain (servo2_val, 1000, 2000);
In part one of the post above, (scroll up and look at the headings in bold) there is a greyed box that contains the code for two functions, one that writes a vlue to eeprom Nd another that reads it back again. write youself a new empty test sketch that uses these two functions to write, read and then serial.print two values. Once you are happy that that works, add it to your sentimental sketch. You will need to include the eeprom library as well, to do this, just copy the line of code that begins with #include and mentions eeprom between the quotation marks. Duane B
ReplyDeletethanks bud.
ReplyDeletemuch appreciated
Hi, I can't have any outputs. I've checked power, common ground, libraries.. If I use your others sketches Rc related, everything works fine. Where can be my mistake? I'm using a Uno, with the signals from the receiver connected to digital in 2 and 3. With an oscilloscope all the outputs are 0.
ReplyDeleteTks for sharing your projects
Have you added the program button and an LED to tell whether you are in program mode or running mode ?
ReplyDeleteDuane B
Hi, I couldn’t find info of how to connect the led, but the output pin are all 0. I’ve used the pin 2 and 3 as input from the receiver, pin 10 and 11 as enable for the L293D, pin 5 and 6 for the left motor, 7 and 8 for the right and I’ve tried to connect the pin 9 on either high and low level to try to enter the program mode. Shouldn’t the board exit in any case from the program mode after 10 seconds? Arduino and L293 are connected to different power sources and the grounds are in common.
DeleteThe code assumes a program button is pressed and then released, if you are just holding a pin low, the code will not exit program mode and write the values to eeprom. As for the LED, you could just use the built in LED on pin 13 - digitalWrite(13,gMode == MODE_PROGRAM);
ReplyDeleteDuane B.
I’ ve found a bad connection on the pull-up resistor. Now everything works fine! Tks again for sharing your projects and for your patience..
DeleteThis code is a bit advanced for me, but seems to work pretty good. I'm using a cheap transmitter that doesn't go back to the exact values when in neutral position. Where can I increase the size of the neutral zone range? When I release the stick I have to tap on the transmitter a few times to stop the jitters.
ReplyDeleteTry increaseing the DEADBAND, this is a range of values around neutral that should be considered as neutral for the exact reason you have described, change -
ReplyDelete#define RC_DEADBAND 40
to
#define RC_DEADBAND 100
Duane.
I cant calibrate my system. I use a arduino nano. Pin 9 is wired with pullup resistor. I turn on the arduino then i push the button for 1 second. Then wait for zero point and switch throttling steering in max and min position. But nothing ist write to eeprom. The system stoppt and i will be push the Reset for reboot. After reboot only standart is now. Sorry for my bad englisch.
ReplyDeleteI build the projekt for my ship with 3 propeller. Can you give me an example where can i create the code for the middle propeller. He will be turn proportional from throttle channel and no mix from steering. I hope anybody understand what i mean.
DeleteI guess you figured out the calibrate button ?
DeleteIf you want power to the middle prop to be proportional to throttle, cant you just write to the throttle channel the value you read from the throttle channel ? or even connect the throttle in from the receiver directly to the middle prop esc ?
Duane B
This is a great post and looks like exactly what I want to do with my bot. The coding is beyond me for the moment, as i am usually building rc planes.
ReplyDeleteI think I have got most of how to implement this down except for the program button. It looks like you have connected program button to pin 9. Can you tell me how to do this, I cant find it in any of your posts? Do you connect 9 to the button and then via resistor to the ground? What value resistor?
Also, I am using an L298 Motor driver, will this work? (http://www.vetco.net/catalog/#gsc.tab=0)?
Thank you,
hi, first of all ty for your work!!.
ReplyDeleteim new working with rc, i read some off your blog but i cant make work me proyect.
I have:: one servo for gear, one L298N with one motor, one am reciver with 2 channels (min 1000, max 1800 and 1400 center, badband 40) and i cant do work with your code.
Have you a code ready for work?¿ :D; or can you help me making it¿?¿?
else what i have to do for use PROGRAM_PIN?; i need add one button to arduino, and press it when i turn on the car?
ReplyDeleteI'm trying to do a similar control algorithm, tank drive with 2 RC channels in, 2 ESC channels out.
ReplyDeleteI'm having a problem with discontinuity: when the throttle channel goes to idle while the steering is outside the deadband the code switches from "limit one side" to "rotate in place". This makes the speed controllers go from low values (1600,1500) to high values (2000, 1000).
I know this isn't an issue if the steering control is zeroed out before rotating, but it definitely presents a safety concern (slow turning to full-speed rotating).
Any ideas?
I am hoping you are still responding to this post. I have figured out the program button, but I am now having trouble keeping it running for more than say 20 seconds. I am using an l298 motor board and am wondering if this is the problem. the bot will run when i push the left stick my Futaba rc plane transmitter up but as soon as i pull it back the programs rests or dumps and i have to push the program button all over again with the same results. please help.
ReplyDeleteI have the code working on my Arduino Duo. I would like to get the code to work on my Attiny84A running arduino. I have a bootloader installed and I swapped the pinouts / uploaded the code but it does not function at all. I suspect that there is an issue with the interrupts I am trying to use PA0 and PA1 for the inputs but I am unsure what is wrong. Can you help me out?
ReplyDeleteI had to adjust the deadband to 200 in order to minimize(but not completely stop) the motor humming. Seems like the motors are never in a stop position even when I'm not touching the controller.
ReplyDeleteDIAC provides Classroom training on PLC, SCADA, HMI, DCS, AC Drives- Motors, Panel Designing, Instrumentation. Get 100% life time job assistance.Call @91-9310096831
ReplyDeletehai, i wanna ask about the program mode pin 9 in your code. where is that pin goes to?
ReplyDeleteDIAC being certified training partner of Panasonic provides professional PLC/ SCADA Training with global certifications, which means; at the end of the training you will get the training certificate of DIAC. Call us 91-9953489987.
ReplyDeleteHi everybody I am new in RC projects and I try to build my robotic tank with mechanical arm. Everything already assemble only problem with simple radio control:( i looking for design about 1 month already and nobody help me :( this is my stuff what i use for my project take look and tell me if its compareble parts because I'm desperate... i found cod for my arduino uno but code only for one motor its compleatly diferent from this one because using pulse input... help HELP HELP please guys !!
ReplyDeleteThis is my stuff:
https://drive.google.com/open?id=1kOdYtD1vgJkfs9UifvP-R_IwXaj4XJLW
1. arduino uno https://www.amazon.com/gp/product/B07MMMSNYH/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1
2. L298N https://www.amazon.com/gp/product/B01M29YK5U/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1
3. tank https://www.amazon.com/gp/product/B07M89R89B/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1
4. Fly Sky control https://www.amazon.com/gp/product/B01EZJBTAG/ref=ppx_yo_dt_b_asin_title_o06_s00?ie=UTF8&psc=1
5. Arm i want use separatly just show you https://www.amazon.com/gp/product/B074T6DPKX/ref=ppx_yo_dt_b_asin_title_o07_s00?ie=UTF8&psc=1
the code https://www.youtube.com/watch?v=v_N5HmXmDyk&t=3s is working i just upload into arduino and go but only for 1 motor :( it is posible to modify this code for 2 motors >??
DeleteMay be easy to buy this drive and forget about arduino ? Sabertooth Dual 5A R/C 6-18V Regenerative Motor Driver
ReplyDeletehttps://www.amazon.com/Sabertooth-6-18V-Regenerative-Motor-Driver/dp/B071Z9K7LX?pd_rd_wg=voE8j&pd_rd_r=0f521d14-a267-4315-9d48-f08601a8454a&pd_rd_w=HcwpR&ref_=pd_gw_cartx&pf_rd_r=R3Z91F0HN7AYVBZ2E6GX&pf_rd_p=15fbf69c-7fbf-5689-b003-d680d779fe52
Listed here you'll learn it is important, them offers the link in an helpful webpage: RC car steering servo
ReplyDeletePharmamedsstore is the Best place to Buy Modalert Online and to also buy other medicines online with overnight delivery. Modalert 200 is made by Sun Pharma. it is an energiser drug that improves individuals’ memory, Vigilance, and attentiveness
ReplyDeletemodalert 200 mg by sun pharma
sun pharma modalert
Don’t stop talking about these topics. It’s just too important for us. My mind feel better after reading this. So give it to thumbs up!
ReplyDeleteour Pharmacy provides valuable service so we operate online to keep our costs low and pass the savings on to you.
ReplyDeleteWe supply branded medicines and our packing totally discreet, most secure, vacuum sealed, and airtight.
100% Customer Satisfaction is guaranteed.Your orders, your personal details are 100% Secure & confidential, Fast & Safe Worldwide Delivery.
We also supply in bulk. For Prices and Place your orders with us .
I really enjoy studying on this site, it holds fantastic blog posts
ReplyDeleteBuy Crystal Meth Online with bitcoin USA
You should keep it up forever! Best of luck.
Your blog was really good and I gain good knowledge from your post. Good work!
ReplyDeleteBankruptcy Lawyers Near Me
Divorce Without Separation Agreement
This blog is very creative and informative
ReplyDeletebancruptcy lawyers near me
bankruotcy lawyer near me
Really knowledgeable blog and keep sharing
ReplyDeletelocal family lawyers
Abogados Divorcio Culpeper VA
Abogados Divorcio Spotsylvania VA
Ankara
ReplyDeleteAntalya
istanbul
Ordu
izmir
JFTU
ankara parça eşya taşıma
ReplyDeletetakipçi satın al
antalya rent a car
antalya rent a car
ankara parça eşya taşıma
İA8L2
Adıyaman Lojistik
ReplyDeleteTrabzon Lojistik
Muğla Lojistik
Bayburt Lojistik
Bayburt Lojistik
07CW
E5989
ReplyDeleteAğrı Parça Eşya Taşıma
Konya Evden Eve Nakliyat
Şırnak Parça Eşya Taşıma
Kırklareli Lojistik
Bayburt Parça Eşya Taşıma
F5E30
ReplyDeleteÇerkezköy Parke Ustası
Çerkezköy Evden Eve Nakliyat
İstanbul Evden Eve Nakliyat
Bilecik Evden Eve Nakliyat
Aydın Evden Eve Nakliyat
FB06C
ReplyDeleteArtvin Evden Eve Nakliyat
Urfa Şehir İçi Nakliyat
Yalova Lojistik
Maraş Şehirler Arası Nakliyat
Düzce Şehir İçi Nakliyat
Batman Şehirler Arası Nakliyat
Edirne Lojistik
Şırnak Lojistik
Eryaman Alkollü Mekanlar
637A7
ReplyDeleteKonya Evden Eve Nakliyat
Kayseri Lojistik
Şırnak Evden Eve Nakliyat
Yalova Şehir İçi Nakliyat
Hakkari Evden Eve Nakliyat
Burdur Parça Eşya Taşıma
Kars Lojistik
İstanbul Şehir İçi Nakliyat
Isparta Şehirler Arası Nakliyat
AAA6F
ReplyDeleteKaraman Evden Eve Nakliyat
Etlik Fayans Ustası
Konya Evden Eve Nakliyat
Çorum Şehirler Arası Nakliyat
Çerkezköy Sineklik
Batman Şehirler Arası Nakliyat
Denizli Şehir İçi Nakliyat
Yalova Şehirler Arası Nakliyat
Ankara Lojistik
RC typically stands for Remote Control. In the context of electronics and hobbyist projects, it often refers to remote-controlled devices such as cars, planes, boats, or other models. Arduino is an open-source electronics platform that provides a hardware and software ecosystem for creating interactive projects. It consists of a microcontroller board, a development environment, and a community of users and developers.
ReplyDeletecontract dispute definition
Unraveling the intricacies of divorce in virginia? Find essential insights, legal guidance, and resources tailored to your unique situation. Navigate the process with confidence and clarity.
ReplyDeleteLearn what constitutes reckless driving in virginia. Explore the legal aspects and consequences of reckless driving offenses in the state.
ReplyDelete4542E
ReplyDeleteantep kadınlarla sohbet et
maraş görüntülü sohbet canlı
adana chat sohbet
konya parasız sohbet
kadınlarla görüntülü sohbet
ücretsiz sohbet siteleri
canlı görüntülü sohbet uygulamaları
Ağrı Ücretsiz Sohbet Siteleri
Çankırı Mobil Sohbet Bedava