Showing posts with label Arduino Interrupts. Show all posts
Showing posts with label Arduino Interrupts. Show all posts

Saturday, August 4, 2012

Multiplexing RC Channels with Arduino

Creative Commons License
RCArduinoChannels by DuaneB is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.


RC Transmitters multiplex as many as 9 channels onto a single radio signal. Your RC Receiver decodes this single signal to obtain the individual channel signals for your servos and electronic speed controllers.


If we reverse part of this process we can read all of the incoming channels using just two Arduino Pins.


Not only does this keep our pins free for more important tasks within our projects, but it is also more efficient to read them this way, we simply need to count the pulses to know which channel we are looking at.

This is a technique I have been using in the development of the RCArduino library.


The library uses just two interrupt pins to read as many as six RC Channels. The library is also able to generate upto six RC Channel outputs in its current form. I will soon be adding the capability to drive 9 servos using only 3 Arduino digital pins. 

UPDATE : The library has been reworked so that it now supports three output modes -

1) Generate six outputs using a selected port 
2) Do not generate any outputs, but operate in a mode compatible with the existing servo library and all of the flexibility that it offers - also suitable for working with H-Bridges etc.
3) Serial servos, use less pins and less code to drive more servos - details soon.


So, if you want to be able to do more, with less, with a library that is smaller and faster than the existing general purpose libraries, read on.

The RC Arduino Channel Multiplexer
Some high end receivers offer a 'raw' output, mine don't and yours don't need to either. Instead of modifying the receiver we will simply create a small hardware 'RC Channel multiplexer'. The multiplexer is basically a single diode for each channel.

The key to the multiplexer is that we route all of the even numbered channels 0,2,4 to INT0 (digital pin 2) and all of the odd numbered channels 1,3,5 to INT1 (digital pin 3).

How does it work ?The diodes stop interference between the pulses - if one channel is high and the others are low, the low channels will sink some of the current from the high channel, this would be sufficient to stop the Arduino from detecting the pulse. However adding a diode to each channel prevents any current from the high channel sinking back into the low channels and ensures we can detect all of the channel pulses.

An RC Arduino Channel Multiplexer - 3 Channels into two pins, as simple as that.
Multiplexing the receiver channels into the two Arduino interrupts is as simple as adding a diode to each channel before connecting it to INT0 for 0 and even numbered channels and INT1 for odd numbered channels.


There is an interesting effect where a charge gets trapped between the diodes and the Arduino pins. To overcome this effect a high value resistor can be place between the point where the diodes meet and ground. I am using a 1M resistor. Picture below -.



Schematic - The even half of the channel multiplexer, the odd half is a duplicate but is connected between the odd numbered channels and INT1 (digital pin 3).



The next post in this series will be a conversion of a previous project to use the new library, as always the post will include the full source code of both the library and the project.

Duane B

Friday, August 3, 2012

Never Say Never - The RC Arduino Library

I always told myself I would never write an Arduino library but after looking at the assembly code of some of my recent projects I have changed my mind.

Most of my projects involve reading and writing RC Signals, these are both time critical activities, the difference between full brakes and full throttle is only one thousandth of a second.

Existing solutions such as the code I have posted previously are perfectly good, but they are based on general purpose libraries that sacrifice performance and accuracy for flexibility.

Examples using general purpose libraries -
Servo Library
http://rcarduino.blogspot.com/2012/01/can-i-control-more-than-x-servos-with.html

PinChangeInt library
http://rcarduino.blogspot.com/2012/03/need-more-interrupts-to-read-more.html
http://rcarduino.blogspot.com/2012/04/how-to-read-multiple-rc-channels-draft.html


I am in the testing stages with a dedicated RC Library which sacrifices some flexibility for a big improvement in accuracy, size and performance.

I have a the perfect test bed for the library in my existing projects -

The L293D RC Robot
http://rcarduino.blogspot.com/2012/05/rc-arduino-robot.html

RC Race Car Child Mode
http://rcarduino.blogspot.com/2012/01/traction-control-part-13-we-have.html

Active Yaw Control
http://rcarduino.blogspot.com/2012/07/rcarduino-yaw-control-part-2.html

By converting each project to use the new library I can demonstrate the performance, ease of use and reliability of the library. I also plan to include the existing projects as samples within the library download.

Bench testing is complete, road testing starts tomorrow.

Duane B


Friday, July 27, 2012

Lap Timer Build Along Part One

Creative Commons License
Based on a work at rcarduino.blogspot.com

Update - I have three big ideas for a version 2.0 of the lap timer system, they build on the capabilities of version 1.0, extending its functionality to a completely new area and adding what I hope will be a really cool user interface feature.

Before I begin work on version 2.0 I am completing the Version 1.0 Build Along - Starting right now.

What do we get at the end of Part One -

 - Record and review race and practice sessions of upto 500 Laps
 - Scroll through summaries of each session to view best and average lap times and the number of session laps
 - Scroll through individual laps within any session
 - A bonus feature for part 1 only - The "Fake A Lap" button
 - The "Fake A Lap" button allows you to test your build before we introduce the automatic lap capture features in  - parts two and three.
 - At the end of part one you will have a fully function manual lap timer - your very own self built multi lap, multi session stop watch !

Build this in Part One today (infra red lap capture in part 2 and build your own transponder in part 3)


Older posts covering the functionality, design and a demonstration using an RC Car can be found here -

Menu and RC Car Demonstration -
Features - 
Interface Design - 

Older videos -
Timing an RC Car fitted with the transponder we will build in part three.
A sample of the session and lap review menus





Everything in version 1.0 will be used as the basis for version 2.0. So lets get started -

Personal Lap Timer V1.0 Part 1
Goal - Build the user interface

Requirements - If you can grab a set of parts that looks like this, you can build part one of the lap timer in one hour.
- An Arduino UNO or compatible (Arduino UNO recommended for beginners)
- An LCD Display
- A 10K Potentiometer
- Hook up wire, breadboard or strip board for connections.
- Four push buttons
- Four 10K pull up resistors for use with the push buttons
- If you want to add the 'Fake A Lap' button you will require one extra button, this button is not required in later stages.







About LCD Displays
There are several libraries for driving LCD Displays with Arduino including one which is installed by default as part of the Arduino application. This is the library we will use, it provides an easy to use interface for driving 'character' LCD displays. These are LCD Displays that already know how to show text and common characters. To keep things simple, the lap timer uses a very common 16*2 character display giving us two lines of sixteen characters to display our race and setup information.

If you do not have an LCD, most electronics suppliers will have them in stock. The approximate cost is 10 dollars or 7 Pounds, if you buy the LCD from a project orientated supplier they will tend to include a 10K potentiometer used to adjust the contrast and some headers for soldering.

LCDs are well known and commonly used components within the micro controller community, for two very good introductions see the links copied below -

Oomlout - A UK Based supplier with downloadable PDF tutorials (scroll to the end of the page) -

Adafruit - A US Based supplier with online tutorials -

Lap Timer Build Along Part One
Step One - Power Connections

1) Connect the red positive power wire from the Arduino 5V Pin to the red positive rail of the breadboard
2) Connect the black ground wire from the Arduino GND Pin to the black (mine is blue) ground rail of the breadboard
3) Check that there are no breaks in the breadboard power rail, mine has a break which I am connecting across using the short green and red wires in the picture below.



I am using a double size breadboard, but if you only have access to smaller breadboards its not going to be a problem. See the two small red and green wires in the picture ? They are there to connect the two halves of this double breadboard together, you can do the same with two smaller breadboards to make a larger prototyping area. 

Step Two - Add The Buttons and Pull Up Resistors

1) Add your four push buttons, make sure to place them the correct way, in general push button pins are in a rectangular layout, the pins are connected to each other in the 'long direction'. The push buttons work across the short side, connecting and disconnect the short side as the button is pushed and released. In the picture the short side is pointing up.
2) Add the 10K pull up resistors, here you can see that I am connecting the top right pin on each button to the red power rail through a 10K pull up resistor. When the buttons is pressed - as mentioned in 1) above, it will connect to the pin closest to it, this is what we want so in the next step we will be connecting the closest (top right) to ground. Note that the top right pin is permanently connected inside the button to the bottom right pin, this is a common mistake to make - the button is permanently on if you position it the wrong way.


Step Three - Add The LCD

1) If your LCD Has headers installed you can plug it straight into your bread board at this point, if not solder in the headers and then plug it in. If you do not have headers, you can solder jumper wires to the LCD and connect it this way. I often use cut off sections of old printer ribbon cables for these types of connections.


Step Four - Add Ground Connections to the buttons
1) In step two we placed our four buttons (menu up,down,ok and cancel) and connected them through a 10K pull up resistor to the red positive rail of the breadboard. Now we want to connect the other side of the button to the black ground rail of the breadboard. Throughout the build along I am using red wire for positive connections and green wire for ground connections, it will make your own build easier if you follow a similar convention.

To understand why we are connecting the buttons this way, read any of the many Arduino Button Tutorials.





Step Five - Add The LCD Power Connections
1) We need to provide power to the LCD and the LCD Back light, to do this on most Arduino compatible LCDs we need to connect the outer two LCD connections to the ground rail and the connections immediately inside each ground connection to the red positive power rail. For alternative explanations of the same wiring refer to the LCD Links provided earlier in the post. There is one additional connection we need to make which is to connect the read write pin (R/W) to ground, we are only ever writing to the LCD so can hold this low. The end result whichever instructions you find easiest to follow should match the picture.


Step Six - Add The LCD Contrast Adjustment

As mentioned in the LCD Section at the start of the post, if you buy your Arduino compatible LCD from a project orientated supplier they will generally include a header for breadboard connections and a 10K potentiometer to adjust the contrast. If you didn't get a potentiometer with you display, any 10K potentiometer will do

1) Connect the left pin of your potentiometer to the red power rail and the right pin to the ground rail of the breadboard.
2) Connect the center pin of your potentiometer to third pin from the left of the LCD - assuming your LCD Orientation matches the picture. I have used orange wire for this connection in the picture.

This connection allows us to adjust the contrast of the LCD, very useful in different light conditions.





Step Seven - Connect the LCD To your Arduino

1) Use jumper wires to connect the LCD Pins to your Arduino, on the Arduino side I am using digital pins 7,8,9,10,11 and 12.
2) On the LCD, these pins are connected as follows -

All LCD Pins are numbered counting from left to right starting with 1.


Arduino PIN LCD PIN
Digital Pin 12 Register Select (Pin 4)
Digital Pin 11 PIN Clock/Enable (Pin 6)
Digital Pin 10 Bit 4 (Pin 11)
Digital Pin 9 Bit 5 (Pin 12)
Digital Pin 8 Bit 6 (Pin 13)
Digital Pin 7 Bit 7 (Pin 14)


The camera has fisheyed the picture slightly so it looks as if the connections on the left are one or two pins further left than they are. Follow the connections in the table and you will be fine.

Step Eight - Progress Check - Hello World

At this point you should be able to run any of the LCD Examples from the Arduino application. A good starting point is to check the connections with the 'helloworld' sketch. To do this -

1) Start the Arduino Application
2) In the File menu, select - Examples/LiquidCrystal/HelloWorld
3) You should now be looking at the example sketch 'HelloWorld' which is part of the Arduino download.
4) Find the line in the sketch that initialises the LCD Object, it should look something like the following -

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);


This initialises an object named lcd of the class LiquidCrystal. As part of the initialisation our program is telling the object which pins to use. We want to use different pins, so we need to replace the line above with the following -


// Initialise the LCD using 12,11,10,9,8,7
//
LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // Original PINs


LiquidCrystal lcd(12,11,10,9,8,7);

Now upload the sketch and if you have connected everything correctly you should see Hello World and the time displayed on you LCD. If not, double check the connections and try again.

Step Nine - Connect Buttons and Finish

1) Connect the four buttons by connecting a jumper wire between the pull up resistor and the top left pin of each button.
2) The buttons should be connected to the Arduino as follows -

Arduino PIN Lap Timer Button (buttons from left to right)
Digital Pin 6 Menu Up (Left Most Button)
Digital Pin 5 Menu Down
Digital Pin 4 Ok
Digital Pin 3 Cancel (Right Most Button


Step Ten - Upload

1) At this point we have completed Part One of the Lap Timer Build Along and can upload the Part One sketch to our build - I originally intended to include some test data so that at the end of Part One you would be able to use the system to scroll through some sessions and review the best, average and individual lap times within a session but then I thought - why not just add one more button ?

Bonus Step - The Big Red "Fake A Lap" Button


The lap timer uses infra red beacons or an infrared transponder to detect laps, we will get to detecting these  in step two and building one in step three.

If we add one more button we can use the system as a manual lap timer right now on day one of the build.

1) Add the 'Fake A Lap' button using the same approach as the other four buttons - 10K pull up resistor etc.
2) Connect the button to the Arduino by adding a jumper from the button to digital pin 2. We are using interrupts to detect laps and so it must be digital pin 2.





The Code -

There are three files lapTimerBuildAlongPart1.pde, LapTimes.cpp and LapTimes.h

If it looks like a lot of code, it isn't, its mostly comments to explain what the code does.

LapTimerBuildAlongPart1.pde -

// RCArduinoPersonalLapTimer Part 1 by DuaneB is
// licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// Based on a work at rcarduino.blogspot.com.

#include <avr/pgmspace.h>
#include <EEPROM.h>
#include <LiquidCrystal.h>
#include "laptimes.h"

//*******************************************************************************************
// USER INTERFACE DEFINITIONS
//*******************************************************************************************
// initialise the liquidCrystal Library

// Initialise the LCD using 12,11,10,9,8,7
LiquidCrystal lcd(12,11,10,9,8,7);

// PINs for user interface buttons - use any
#define KEY_OK_PIN     6
#define KEY_CANCEL_PIN     5
#define KEY_UP_PIN     4
#define KEY_DOWN_PIN     3

// bit flags used in key functions getKeys, waitForKeyPress, waitForKeyRelease
#define KEY_NONE     0
#define KEY_OK         1
#define KEY_CANCEL     2
#define KEY_UP         4
#define KEY_DOWN     8

#define KEYPRESS_ANY B11111111

// display width + 1, used by getRamString to copy a PROG_MEM string into ram
#define DISPLAY_ROW_BUFFER_LENGTH 17

//*******************************************************************************************
// Lap Capture definitions
//*******************************************************************************************
#define LAP_CAPTURE_LED 13
#define BUZZER_PIN A0

// minimum and maximum duration of qualifying IR Pulse
#define MIN_PULSE_DURATION 200
#define MAX_PULSE_DURATION 500

// start and end of pulse
uint32_t ulStartPulse;
uint32_t ulEndPulse;
volatile uint32_t ulPulseDuration;

// flags to manage access and pulse edges
volatile uint8_t bIRPulseFlags;
//
volatile uint32_t ulNewLapStartTime;

#define IR_PULSE_START_SET 1
#define IR_PULSE_END_SET 2

//*****************************************************************
// Global Instance of CLapTimes class
//*****************************************************************
CLapTimes gLapTimes(new CEEPROMLapStore());

//////////////////////////////////////////////////////////////////////////////////
//
// doShowSessionSummaries
//
// implements the show session summary menu
// allows the user to scroll up and down through summaries of the recorded sessions
//
//////////////////////////////////////////////////////////////////////////////////
void setup()
{
 Serial.begin(9600);
 Serial.println("In Setup");
  
 lcd.begin(16, 2);
 lcd.print("Lap Timer");
 lcd.setCursor(0,1);
 lcd.print("Version 0.9 Beta");

 delay(3000);

 pinMode(KEY_OK_PIN,INPUT);
 pinMode(KEY_CANCEL_PIN,INPUT);
 pinMode(KEY_UP_PIN,INPUT);
 pinMode(KEY_DOWN_PIN,INPUT);

 pinMode(LAP_CAPTURE_LED,OUTPUT);
 pinMode(BUZZER_PIN,OUTPUT);

 digitalWrite(LAP_CAPTURE_LED,LOW);
 digitalWrite(BUZZER_PIN,LOW);

 showTotals(); 
 
 Serial.println("Out Setup");
}

//////////////////////////////////////////////////////////////////////////////////
//
// base loop, implements root of menu system
//
// allows the user to scroll up and down through summaries of the recorded sessions
//
//////////////////////////////////////////////////////////////////////////////////
void loop()

  // lets keep control of the loop
  while(true)
  {
   // wait for a key command to tell us what to do
   Serial.println("Beginning Loop");
   switch(waitForKeyPress(KEYPRESS_ANY))
   {
    // start recording
    case KEY_OK:
     doRecord();
     break;
    // delete all sessions
    case KEY_CANCEL:
     doConfirmDeleteSessions();
     break;
    // scroll through recorded session summaries
    case KEY_UP:
    case KEY_DOWN:
     doShowSessionSummaries();
     break;
   }

   showTotals();
   
   waitForKeyRelease();
  }
}

//////////////////////////////////////////////////////////////////////////////////
//
// doRecord
//
// start recording new sessions, update screen every second
// check for new laps
// record new laps
// show lap time for a few seconds at the end of a lap
// update and show new best lap if its a new session best
//
//////////////////////////////////////////////////////////////////////////////////
void doRecord()
{
  lap_handle_t currentLapHandle = gLapTimes.createNewSession();
 
  uint32_t ulOldLapStartTime = millis(); 
  lap_time_t bestLapTime = 0XFFFF;
 
  uint32_t ulLastTimeRefresh = millis();
  char *pStringTimeBuffer = NULL;
 
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print(getRamString(PSTR("Recording")));

  attachInterrupt(0,captureLap,CHANGE);
 
  while((getKeys() != KEY_CANCEL) && (currentLapHandle != INVALID_LAP_HANDLE))
  {
    Serial.println(ulPulseDuration);   
    //////////////////////////////////////////////////////////////////////////////////////////////////////
    // Check for new laps captured
    //////////////////////////////////////////////////////////////////////////////////////////////////////
    if((IR_PULSE_END_SET|IR_PULSE_START_SET) == bIRPulseFlags)
    {
      uint32_t ulLastLapDuration = ulNewLapStartTime - ulOldLapStartTime;
      ulOldLapStartTime = ulNewLapStartTime;
     
      lap_time_t lapTime = CLapTimes::convertMillisToLapTime(ulLastLapDuration);

      gLapTimes.addLapTime(currentLapHandle,lapTime);       
      currentLapHandle = gLapTimes.moveNext(currentLapHandle);       

      // new best lap     
      if(lapTime < bestLapTime)
      {
        bestLapTime = lapTime;
      }
     
      lcd.clear();
      lcd.print(getRamString(PSTR("Best Lap ")));
      lcd.print(CLapTimes::formatTime(bestLapTime,true));
     
      lcd.setCursor(0,1);
      lcd.print(getRamString(PSTR("Last Lap")));
      // use this to show lap time
      lcd.print(CLapTimes::formatTime(lapTime,true));
      // or this to show delta time
      //lcd.print(CLapTimes::formatTime(lapTime-bestLapTime,true));
     
      digitalWrite(LAP_CAPTURE_LED,HIGH);
      digitalWrite(BUZZER_PIN,HIGH);
      delay(400);
     
      if(lapTime == bestLapTime)
      {
        digitalWrite(LAP_CAPTURE_LED,LOW);
        digitalWrite(BUZZER_PIN,LOW);
        delay(200);
        digitalWrite(LAP_CAPTURE_LED,HIGH);
        digitalWrite(BUZZER_PIN,HIGH);
        delay(400);
      }
     
      digitalWrite(LAP_CAPTURE_LED,LOW);
      digitalWrite(BUZZER_PIN,LOW);
                     
      // dont look for another lap for 2 seconds
      delay(2000); 
     
      // give ownership of the shared variables back to the ISR
      bIRPulseFlags = 0;
    }
       
    //////////////////////////////////////////////////////////////////////////////////////////////////////
    // Update screen with current lap time
    //////////////////////////////////////////////////////////////////////////////////////////////////////
    uint32_t ulCurrentLapTime = millis();
    if((ulCurrentLapTime - ulLastTimeRefresh) > 1000)
    {
      ulLastTimeRefresh = ulCurrentLapTime;
     
      lcd.clear();
     
      if(bestLapTime != 0XFFFF)
      {
       lcd.print(getRamString(PSTR("Best Lap ")));
       lcd.print(CLapTimes::formatTime(bestLapTime,true));
      }
      else
      {
       lcd.print(getRamString(PSTR("Recording")));
      }
     
      pStringTimeBuffer = CLapTimes::formatTime(CLapTimes::convertMillisToLapTime(ulCurrentLapTime - ulOldLapStartTime),false);
      if(pStringTimeBuffer != NULL)
      {
        lcd.setCursor(0,1);
        lcd.print(pStringTimeBuffer);
      }
      else
      {
        // If we do not complete a lap for 9m59s display an idle message until a key is pressed
        lcd.setCursor(0,1);
        lcd.print(getRamString(PSTR("Idle")));
        waitForKeyPress(KEYPRESS_ANY);
        ulOldLapStartTime = millis();
      }
    }
  }
 
  if(currentLapHandle == INVALID_LAP_HANDLE)
  {
    lcd.setCursor(0,1);
    lcd.print(getRamString(PSTR("Memory Full!")));
  }
}

//////////////////////////////////////////////////////////////////////////////////
//
// doConfirmDeleteSessions
//
// Delete all sessions - if we are using storage for the first time we may need
// to call this function to initialise the storage to a known value. The user
// can access this function by pressing cancel on the root menu. This will bring
// up a confirmation message asking the user to press ok to delete all laps.
//
//////////////////////////////////////////////////////////////////////////////////
void doConfirmDeleteSessions()
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print(getRamString(PSTR("OK to Reset")));
  lcd.setCursor(0,1);
  lcd.print(getRamString(PSTR("Cancel to go Back")));

  // we pressed cancel to get here - so lets wait for cancel to be released before we look for more input
  waitForKeyRelease();
 
  if(KEY_OK == waitForKeyPress(KEY_OK|KEY_CANCEL))
  {
   gLapTimes.clearAll();
  }
}

//////////////////////////////////////////////////////////////////////////////////
//
// doShowSessionSummaries
//
// implements the show session summary menu
// allows the user to scroll up and down through summaries of the recorded sessions
// user can press ok to enter the session and scroll through the session laps
//
//////////////////////////////////////////////////////////////////////////////////
void doShowSessionSummaries()
{
 boolean bFinished = false;
 uint8_t nSession = 0;

 do
 {
  lap_handle_t lapHandle = 0;
  uint16_t nSessionAverage = 0;
  uint16_t nSessionBest = 0;
  uint16_t nSessionLapCount = 0;

  Serial.println(nSession);
  
  lapHandle = gLapTimes.getSessionHandle(nSession); 
 
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print(getRamString(PSTR("SNo:")));
  lcd.print(nSession);
 
  // if theres no laps for this session or its the first session but it doesnt contain any laps
  if(lapHandle == INVALID_LAP_HANDLE || (lapHandle == 0 && gLapTimes.getLapTime(lapHandle)==0))
  {
    lcd.setCursor(0,1);
    lcd.print(getRamString(PSTR("Empty Session")));
  }
  else
  {
    Serial.println(lapHandle);
   
    gLapTimes.getSessionSummary(lapHandle,nSessionAverage,nSessionBest,nSessionLapCount);
   
    lcd.print(getRamString(PSTR(" Laps:")));
    lcd.print(nSessionLapCount);
    lcd.setCursor(0,1);
// Best Lap Time
    lcd.print(CLapTimes::formatTime(nSessionBest,true));
// Average Lap Time
    lcd.print(" ");
    lcd.print(CLapTimes::formatTime(nSessionAverage,true));
  }

  waitForKeyRelease();

  switch(waitForKeyPress(KEYPRESS_ANY))
  {
   case KEY_UP:
    nSession++;
    break;
   case KEY_DOWN:
    nSession--;
    break;
   case KEY_CANCEL:
    bFinished = true;
    break;
   case KEY_OK:
    if(nSessionLapCount != 0)
    { 
      doLapScroll(gLapTimes.getSessionHandle(nSession));
    }
    break;
  }
 }while(!bFinished);
}

//////////////////////////////////////////////////////////////////////////////////
//
// showTotals shows the number of sessions, laps and laps left
// as the root of the menu
//
//////////////////////////////////////////////////////////////////////////////////
void showTotals()
{
 Serial.println(getRamString(PSTR("Entering showTotals")));

 uint16_t nSessions = 0;
 uint16_t nLapsRecorded = 0;
 uint16_t nLapsRemaining = 0;

 gLapTimes.getTotals(nSessions,nLapsRecorded,nLapsRemaining);

 lcd.clear();
 lcd.print(getRamString(PSTR("Sessions=")));lcd.print(nSessions);
 lcd.setCursor(0, 1);
 lcd.print(getRamString(PSTR("Laps=")));lcd.print(nLapsRecorded);
 lcd.print(getRamString(PSTR("Left=")));lcd.print(nLapsRemaining);
 
 Serial.println(getRamString(PSTR("Leaving showSummaryData")));
}

//////////////////////////////////////////////////////////////////////////////////
//
// doLapScroll
//
// scroll through the laps within a session, startLapHandle points to the start
//
//////////////////////////////////////////////////////////////////////////////////
void doLapScroll(lap_handle_t startLapHandle)
{
 boolean bFinished = false;
 lap_handle_t currentLapHandle = startLapHandle;
 lap_handle_t tmpLap = currentLapHandle;
 uint8_t nLapNumber = 0;

 do
 {
   lcd.clear();
   lcd.setCursor(0,0);
  
   if(tmpLap == INVALID_LAP_HANDLE)
   {
     lcd.print(getRamString(PSTR("No More Laps")));
     delay(2000);
     lcd.clear();
   }

   lcd.print(getRamString(PSTR("Lap No.")));
   lcd.print(nLapNumber);
   lcd.setCursor(0,1);
  
   if(currentLapHandle != INVALID_LAP_HANDLE)
   {
     char *pTime = CLapTimes::formatTime(gLapTimes.getLapTime(currentLapHandle),true);
     lcd.setCursor(0,1);
     lcd.print(pTime); 
   }

   waitForKeyRelease();
  
   uint8_t sKey = waitForKeyPress(KEYPRESS_ANY);
   switch(sKey)
   {
     case KEY_DOWN:
     case KEY_UP:
      (sKey == KEY_UP) ? tmpLap = gLapTimes.moveNext(currentLapHandle) : tmpLap = gLapTimes.movePrevious(currentLapHandle);
      if(tmpLap != INVALID_LAP_HANDLE)
      {
        if(gLapTimes.getLapTime(tmpLap) != EMPTY_LAP_TIME)
        {
          currentLapHandle = tmpLap;
          (sKey == KEY_UP) ? nLapNumber++ : nLapNumber--;
        }
        else
        {
          tmpLap = INVALID_LAP_HANDLE;
        }
      }
      break;
     case KEY_OK:
      tmpLap = currentLapHandle;
      break;
     case KEY_CANCEL:
      bFinished = true;
      break;
   }
 }
 while(!bFinished);
}

//////////////////////////////////////////////////////////////////////////////////
//
// Key related helpers
//
// getKeys - pole keys
// waitForKeyPress - block waiting for keys based on a mask
// waitForKeyRelease - block waiting until no kets are pressed
//
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//
// getKeys
//
// read the inputs and create a bit mask based on the buttons pressed
// this does not block, need to review whether we should make this block, in most
// cases we loop waiting for a key, sometimes we also loop waiting for no key
// could put both options here with an input parameter.
//
//////////////////////////////////////////////////////////////////////////////////
short getKeys()
{
 // Use bit flags for keys, we may have a future use for
 // combined key presses

 short sKeys = KEY_NONE;
 if(digitalRead(KEY_UP_PIN) == LOW)
 {
  sKeys |= KEY_UP;
 }
 if(digitalRead(KEY_DOWN_PIN) == LOW)
 {
  sKeys |= KEY_DOWN;
 }
 if(digitalRead(KEY_OK_PIN) == LOW)
 {
  sKeys |= KEY_OK;
 }
 if(digitalRead(KEY_CANCEL_PIN) == LOW)
 {
  sKeys |= KEY_CANCEL;
 }

 return sKeys;
}

//////////////////////////////////////////////////////////////////////////////////
//
// waitForKeyRelease
//
// we can enter a function while the activating key is still pressed, in the new
// context the key can have a different purpose, so lets wait until it is released
// before reading it as pressed in the new context
//
//////////////////////////////////////////////////////////////////////////////////
void waitForKeyRelease()
{
  do
  {
    // do nothing
  }
  while(getKeys() != KEY_NONE);

  // debounce
  delay(20);
}

//////////////////////////////////////////////////////////////////////////////////
//
// waitForKeyPress
//
// convenience function, loop doing nothing until one of the sKeyMask keys is
// pressed
//
//////////////////////////////////////////////////////////////////////////////////
uint8_t waitForKeyPress(uint8_t sKeyMask)
{
  uint8_t sKey = KEY_NONE;
 
  do
  {
    sKey = getKeys() & sKeyMask;   
  }
  while(sKey == KEY_NONE);
 
  digitalWrite(BUZZER_PIN,HIGH);
  delay(20);
  digitalWrite(BUZZER_PIN,LOW);
 
  return sKey;
}

// A helper that copies a string from program memory into a buffer in sram
// we need this because our code can only use strings held in sram
// this fetches strings that are stored in program memory as and when
// we need them.
char * getRamString(PGM_P pString)
{
  // NEED TO ADD A CHECK HERE TO ENSURE pString < DISPLAY_ROW_LENGTH
 
  static char pBuffer[DISPLAY_ROW_BUFFER_LENGTH];
 
  return strcpy_P(pBuffer,pString);
}

//////////////////////////////////////////////////////////////////////////////////
//
// captureLap
//
// In part 1 we will fake a lap if we detect INT0 being pulled low
// In part 2 we will add a simple IR Detector which you can test with
// a TV Remote
// In part three we will add a more complex IR Detector which will allow
// us to detect a transponder and ignore any other IR Signal
//////////////////////////////////////////////////////////////////////////////////
void captureLap()
{
  uint8_t bLapCaptureState = digitalRead(2);
  digitalWrite(LAP_CAPTURE_LED,bLapCaptureState);
  if(bLapCaptureState == LOW)
  {
     bIRPulseFlags = (IR_PULSE_END_SET|IR_PULSE_START_SET);
     ulNewLapStartTime = millis();
  }
  else
  {
    bIRPulseFlags = 0;
  }
}


 // LapTimes.cpp

#include "arduino.h"
#include "laptimes.h"
#include <../../../../libraries/EEPROM/EEPROM.h>

// Does what it says, gets a lap time from EEPROM - does not do any validation
lap_time_t CEEPROMLapStore::getLapTime(lap_handle_t lapHandle)
{
  lap_time_t lapTime = (EEPROM.read((lapHandle*sizeof(uint16_t))+1)<<8);
  lapTime += EEPROM.read(lapHandle*sizeof(uint16_t));
 
  return lapTime;
}

// Does what it says, sets a lap time in EEPROM - does not do any validation
void CEEPROMLapStore::setLapTime(lap_handle_t lapHandle,lap_time_t lapTime)
{
  EEPROM.write(lapHandle*sizeof(uint16_t),lowByte(lapTime));
  EEPROM.write((lapHandle*sizeof(uint16_t))+1,highByte(lapTime));
}

// Initialise the lap store to EMPTY_LAP_TIME through out
// this is an important function, we find empty space by looking
// for one EMPTY_LAP_TIME that defines the end of a session followed
// immediatley by another EMPTY_LAP_TIME, this show that there
// are not sessions following the previous session in which case
// we are free to create a new session.
// We cannot be sure what SD, Memory or EEPROM will contain on the first run
// and so it is important we have this option to initialise the storage to
// a known value.
void CEEPROMLapStore::clearAll()
{
  for(uint16_t unIndex = 0;unIndex < (getMaxLaps()*sizeof(lap_time_t));unIndex++)
  {
    EEPROM.write(unIndex,EMPTY_LAP_TIME);
  }
}

// Return the maximum number of laps for this storage media (or device ATMega8,328,1240 etc)
uint16_t CEEPROMLapStore::getMaxLaps()
{
  return EEPROM_LAP_STORE_MAX_LAPS;
}


//*******************************************************************************************
// CLapTimes
//
// A lot of the work in this class is simply finding the start and end of sessions, and
// finding space to start a new session.
//
// With more memory I would have used headers to do a lot of the work inside CLapTimes
// A file system could also have done a lot of the work.
//
// It isn't pretty and could be refactored but it works.
//
//*******************************************************************************************

// Initialise CLapTimes which whichever class we want to provide the actual lap storage
// all lap storage is through the ILapStore interface and so we can use any class
// that implements this interface. Only CEEPROMLapStore is provided in this release,
// others may follow
CLapTimes::CLapTimes(ILapStore *pLapStore)
{
  m_pLapStore = pLapStore;
}
 
// The end of a session is marked by an empty lap (0)
// to create a new session, we first look at the very first lap, if its invalid, there are no sessions
// and we can start a new one from position 0.
// if there is a valid lap at position 0 we need to scan for two consecutive invalid laps. A single invalid lap indicates
// the end of an existing session, if this is followed by anything other than an invalid lap, it is the beginning
// of a new session, if its followed by an invalid lap then we have found the end of the existing sessions and
// can use the second invalid lap handle as the start of our new session.
lap_handle_t CLapTimes::createNewSession()
{
  lap_handle_t newSessionLapHandle = 0;
  lap_handle_t currentLapHandle = 0;
 
  // if the first lap is a valid lap - we need to scan through the recorded laps
  // and sessions to find two consecutive invalid laps - the first we leave in place to
  // mark the end of the existing sessions, the second is a free space for us to create
  // a new session.
  if(m_pLapStore->getLapTime(newSessionLapHandle) != EMPTY_LAP_TIME)
  {
    // assume the worst - there is no space left
    newSessionLapHandle = INVALID_LAP_HANDLE;
   
    // loop until we have a valid lap handle or we reach the end of the lap store
    while(newSessionLapHandle == INVALID_LAP_HANDLE && currentLapHandle < m_pLapStore->getMaxLaps())
    {
     // loop until we reach the end of the lap store or we find an empty lap time
     while(currentLapHandle < m_pLapStore->getMaxLaps() && (m_pLapStore->getLapTime(currentLapHandle) != EMPTY_LAP_TIME))
     {
       currentLapHandle++;
     };
    
     // we found an invalid lap, so check the the next lap handle is less than the end of the lap store
     // and that the content of the next lap is an empty lap meaning it is free for us to use
     if(((currentLapHandle+1)<m_pLapStore->getMaxLaps()) && (m_pLapStore->getLapTime(++currentLapHandle) == EMPTY_LAP_TIME))
     {
       // Yay ! we got two consecutive empty laps so lets set the firstLapHandle so we can start our new session.
       newSessionLapHandle = currentLapHandle;
     }
  }
}

return newSessionLapHandle;
}

void CLapTimes::setLapTime(lap_handle_t lapHandle,lap_time_t lapTime)
{
  m_pLapStore->setLapTime(lapHandle,lapTime);
}

lap_time_t CLapTimes::getLapTime(lap_handle_t lapHandle)
{
  return  m_pLapStore->getLapTime(lapHandle);
}

// scan through all of the recorded laps, total the number of sessions, total the number of laps
// recorded and return and indicative number of remaining laps - its indicative becuase
// each session requires on end of session marker so 10 sessions of 5 laps takes 60 laps
// (10 * 5 laps + 10 end of session markers) one session of 5 sessions of 10 laps takes 55 laps
// (5 * 10 + 5 end of session markers)
void CLapTimes::getTotals(uint16_t &nSessions,uint16_t &nLapsRecorded,uint16_t &nLapsRemaining)
{
  lap_handle_t lapHandle = 0;
  nSessions = 0;
  nLapsRecorded = 0;
  nLapsRemaining = 0;
  
  while(lapHandle < m_pLapStore->getMaxLaps() && (m_pLapStore->getLapTime(lapHandle) != EMPTY_LAP_TIME))
  {
    // we have a session so count it
    nSessions++;
    // and count the laps within the session
    while(lapHandle < m_pLapStore->getMaxLaps() && (m_pLapStore->getLapTime(lapHandle++) != EMPTY_LAP_TIME))
    {
      nLapsRecorded++;
    }
  }
 
  nLapsRemaining = m_pLapStore->getMaxLaps() - nLapsRecorded;
}

void CLapTimes::clearAll()
{
  m_pLapStore->clearAll();
}

// This is similar to get totals but works within a session only, returns the average of all laps in the session,
// the best lap and the total number of laps  
lap_handle_t CLapTimes::getSessionSummary(lap_handle_t lapHandle,uint16_t &nSessionAverage,uint16_t &nSessionBest,uint16_t &nSessionLapCount)
{
  nSessionAverage = 0;
  nSessionBest = 0xFFFF;
  nSessionLapCount = 0;
 
  lap_time_t nLapTime = 0;
  uint32_t nTotalTime = 0;

  while((INVALID_LAP_HANDLE != (nLapTime = m_pLapStore->getLapTime(lapHandle))) && (nLapTime != EMPTY_LAP_TIME))
  {
    nTotalTime += nLapTime;
   
    if(nLapTime < nSessionBest)
    {
      nSessionBest = nLapTime;
    }
   
    nSessionLapCount++;
   
    lapHandle++;
  }

  nSessionAverage = nTotalTime/nSessionLapCount;
 
  return nLapTime;
}

lap_handle_t CLapTimes::addLapTime(lap_handle_t lapHandle,lap_time_t lapTime)
{
  if(lapHandle < m_pLapStore->getMaxLaps())
  {
    m_pLapStore->setLapTime(lapHandle,lapTime);
  }
  else
  {
    lapHandle = INVALID_LAP_HANDLE;
  }

return lapHandle;
}
  
lap_handle_t CLapTimes::moveNext(lap_handle_t lapHandle)
{
  if(lapHandle < m_pLapStore->getMaxLaps())
  {
    lapHandle++;
  }
  else
  {
    lapHandle = INVALID_LAP_HANDLE;
  }
  
  return lapHandle;
}
  
lap_handle_t CLapTimes::movePrevious(lap_handle_t lapHandle)
{
  if(lapHandle >= 1)
  {
    lapHandle--;
  }
  else
  {
    lapHandle = INVALID_LAP_HANDLE;
  }

  return lapHandle;
}

// given a session number, find it the start of the session and return a handle to it  
lap_handle_t CLapTimes::getSessionHandle(uint8_t nSession)
{
  lap_handle_t currentLapHandle = 0;
  uint8_t nCurrentSession = 0;
  uint16_t nLapTime = 0;
  
  while(nCurrentSession != nSession && currentLapHandle < m_pLapStore->getMaxLaps())
  {
    // loop until we read the max laps or we find and empty lap
    do
    {
      currentLapHandle++; 
    }
    while((currentLapHandle) < m_pLapStore->getMaxLaps() && m_pLapStore->getLapTime(currentLapHandle) != EMPTY_LAP_TIME);
    
    nCurrentSession++;
    
    if(currentLapHandle < m_pLapStore->getMaxLaps())
    {
      // move next to step over the 0 terminator for the previous session   
      currentLapHandle++;
      
      // if the first lap of the session is empty there is no session
      // so return invalid lap to indicate no session found.
      if(getLapTime(currentLapHandle) == EMPTY_LAP_TIME)
      {
        currentLapHandle = INVALID_LAP_HANDLE;
      }
    }
    else
    {
      currentLapHandle = INVALID_LAP_HANDLE;
    }
  }
  
  return currentLapHandle;
}

// 10 minutes is 600 seconds or 600,000 milli seconds, this is too big to fit into a uint32_t
// so we divide by 10 to convert the value into a lap_time_t which contains the lap time in 100's
// of seconds.
lap_time_t CLapTimes::convertMillisToLapTime(uint32_t ulTime)
{
  return ulTime/10;
}

// turn a lap_time_t into a time string formatted as - m:ss:dd
// bPrecision turns 100's on or off
char* CLapTimes::formatTime(lap_time_t time,unsigned char bPrecision)
{
  char *pResult = NULL;

  lap_time_t nSeconds = time/100;
  lap_time_t nMinutes = nSeconds/60;
  lap_time_t nHundredths = 0;

  if(nMinutes <= 9)
  {
    if(bPrecision)
    {    
      nHundredths = time - (nSeconds*100);
    }

    nSeconds -= (nMinutes * 60);  

    m_pTimeStringBuffer[7] = 0;
    m_pTimeStringBuffer[6] = (nHundredths%10)+'0';
    m_pTimeStringBuffer[5] = (nHundredths/10)+'0';
    m_pTimeStringBuffer[4] = '.';
    m_pTimeStringBuffer[3] = (nSeconds%10)+'0';
    m_pTimeStringBuffer[2] = (nSeconds/10)+'0';
    m_pTimeStringBuffer[1] = ':';
    m_pTimeStringBuffer[0] = nMinutes + '0';   
   
    pResult = m_pTimeStringBuffer;
  }
 
  return pResult;
}
 
char CLapTimes::m_pTimeStringBuffer[9];/*m:ss:dd - dd represents hundredths of a second */









// LapTimes.h

// If we assume that lap data will always be set to 0
// Session ends will always be 0
// we only ever return invalid handle

//*******************************************************************************************
// Lap storage and retreival definitions
//*******************************************************************************************
#define EMPTY_LAP_TIME 0
#define INVALID_LAP_HANDLE 0XFFFF

typedef uint16_t lap_handle_t;
typedef uint16_t lap_time_t;

//*******************************************************************************************
// ILapStore
//
// Defines a pure virtual class (C++ Terminology) or interface (Java Terminology)
// It simply defines the functions that can be used to get, set and clear laps
// in a lap store.
//
// The following lap store is provided
// 1) CEEPromLapStore - this one stores laps in the EEPROM
// If you wanted to add SD Card Storage you could define a new class CSDCardLapStore
//
//*******************************************************************************************
class ILapStore
{
public:
  virtual lap_time_t getLapTime(lap_handle_t lapHandle) = 0;
  virtual void setLapTime(lap_handle_t lapHandle,lap_time_t lapTime) = 0;
  virtual void clearAll() = 0;
  virtual uint16_t getMaxLaps() = 0;
};

//*******************************************************************************************
// CEEPROMLapStore
//
// Store laps in memory -
//
// For - simple, easy, its just using an array in memory
// Against - lose all laps if power is lost or Arduino is reset
//
//*******************************************************************************************
#define EEPROM_LAP_STORE_MAX_LAPS 500

class CEEPROMLapStore : public ILapStore
{
public:
 virtual lap_time_t getLapTime(lap_handle_t lapHandle);
 virtual void setLapTime(lap_handle_t lapHandle,lap_time_t lapTime);
 virtual void clearAll();
 virtual uint16_t getMaxLaps();
protected:
 lap_time_t m_LapTimes[EEPROM_LAP_STORE_MAX_LAPS];
};

//*******************************************************************************************
// CLapTimes
//
// A lot of the work in this class is simply finding the start and end of sessions, and
// finding space to start a new session.
//
// With more memory I would have used headers to do a lot of the work inside CLapTimes
// A file system could also have done a lot of the work.
//
// It isn't pretty and could be refactored but it works.
//
//*******************************************************************************************
class CLapTimes
{
public:
  CLapTimes(ILapStore *pLapStore);
  lap_handle_t createNewSession();
  void setLapTime(lap_handle_t lapHandle,lap_time_t lapTime);
  lap_time_t getLapTime(lap_handle_t lapHandle);
  void getTotals(uint16_t &nSessions,uint16_t &nLapsRecorded,uint16_t &nLapsRemaining);
  void clearAll();
  lap_handle_t getSessionSummary(lap_handle_t lapHandle,uint16_t &nSessionAverage,uint16_t &nSessionBest,uint16_t &nSessionLapCount);
  lap_handle_t addLapTime(lap_handle_t lapHandle,lap_time_t lapTime);
  lap_handle_t moveNext(lap_handle_t lapHandle);
  lap_handle_t movePrevious(lap_handle_t lapHandle);
  lap_handle_t getSessionHandle(uint8_t nSession);
  static lap_time_t convertMillisToLapTime(uint32_t ulTime);
  static char* formatTime(lap_time_t time,unsigned char bPrecision);
protected:
  ILapStore *m_pLapStore;
public:
  static char m_pTimeStringBuffer[9];/*m:ss:dd - dd represents hundredths of a second */
};
 



Sunday, May 20, 2012

RC Channels, L293D Motor Driver - Part 2 Calibration And Code

Creative Commons License
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.


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 Steering108084.0
Full Right Steering190080.0
Center Steering14843.2
Full Brakes110878.4
Full Throttle191683.2
Neutral Throttle15244.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));
}
 
// 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;
}


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;
 
    unThrottleCenter = unThrottleIn;
    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;
   }
  }

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;
      

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;
      

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;
    }
  }
 

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));
}