Full Version : Winning Line Follower from Sumovore & Atmel
avr >>ROBOTS & AUTONOMOUS VEHICLES >>Winning Line Follower from Sumovore & Atmel


AVR_Admin- 05-08-2006
Making the Sumovore with Atmel brainboard into a killer line follower

I won a Solarbotics Sumovore kit during the 2004 Western Canadian Robot Games. I usually scratch build my own chassis but since this fell into my lap I decided to see what I could do with it. Currently, I plan to make it part of Climber's Murderous Robot Swarm but figured I would start out by seeing how good I can make it follow a line (all Murderous Robot Swarms have to start somewhere.) I adapted the code from my Gunnagitcha competition line-following robot. Although the sensor suite isn't quite as optimized for line following as Gunnagitcha's is I was stll able to make it follow a line very accurately and quite quickly. A bot like this could easily score over a thousand points on the line-following tracks that appeared at the 2004 games.

Because the guys at Solarbotics are so cool I decided to whip up this web page and put it on the net so other owners of sumovores could try this code out and see how well it does at their line following competitions. I assume the reader understands basic electronics and is able to download a program to their bot. A link to the complete source code appears at the end of this web page. I use avr-libc and gcc for all my AVR programming but this code should be easy to adapt for other compilers.

One of my primary goals for this version was to keep the kit as stock as possible and do most of the work in software. The only physical mods were to add ways the user can change system parameters without having to reprogram the bot by adding a couple of configuration jumpers. Version two of the Killer Sumovore Line Following Robot will have more mods.

First, make a configuration jumper. The brainboard has three headers for auxilliary functions or servos. We'll make use of header three. Take a spare .1 inch center three-pin female header, snip out the center pin and solder a 500 ohm resistor between the two remaining pins. When this "jumper" is installed on the header it will pull that corresponding pin on the microcontroller low. When removed the mega8's internal pullup resistor will pull it high. Header three will be used to choose the bot's speed. Some really complex tracks are tough to stay on the line when going at top speed. By default the bot will go slowly for maximum accuracy. Installing the jumper will make the bot go top speed.

IMPORTANT: do not be tempted to solder a wire between the pins on this jumper. If you make a mistake in the software and output a value to the corresponding pin the jumper is attached to you could end up with a short circuit and all the sumovore's magic smoke will then escape. The 500 ohm resistor ensures that if a mistake is made only 10 milliamps will flow at most. That's well within the safety zone. Because the mega8's internal pullup resistors are 10-20k a 500 ohm resistor is small enough so that the tap into the resulting voltage divider is close enough to ground to be visible to the processor as a low.

Now let's take a look at the software. HERE is the code. Print it out and follow along as I describe it below.

When I program a bot using the Atmel processor I always have a function called init. This function does all of the setup that I like to get done. It sets the output ports, turns on any internal pullups, sets up internal registers and other stuff that is usually only called once. Note that I have a big comment block at the beginning that has the pinout of the processor I am using. Very handy to have.

The motortab table was something I put in way back when I noticed that two motors could run at slightly different speeds at the same level of PWM. That would mean when I programmed the bot to go straight it wouldn't. Kind of annoying. Normally, when you want the motors to run at a predictable speed you measure the RPM and adjust accordingly. I don't happen to have an encoder on my sumorvore's wheels (yet) so I compensate by putting in a table like this that I can tweak later. At this time the motors seem to run ok so I will leave the table symmetrical but leave it in in case I want to change it later.

There are three indexes to this table. The first is for the speed we want to run the motor at relative to the top speed, the second is to choose the overall bot top speed and the last is for which motor, left or right. The top speed is set by the configuration jumper I mentioned above. Normally I have four top speeds for my bots but, for now, we will confine ourselves to just two. When the jumper is installed speed will be set to 1 (the max).

We have 5 sensors out front to detect white/black surfaces. What I did is divide these sensors into two groups. The middle three are for pretty ordinary line following. If the middle sensor sees the line go straight, if the right sensor sees the line turn right and turn left if the left sensor sees the line. That simple strategy will follow just about any simple line track. However, if we want to compete with this bot we need to be able to deal with more than that. The tough tracks have gaps, switchbacks and 90 degree turns. It is for this we need to use the outermost sensors for a very special job: record if they saw a line recently off to either side.

When the bot comes across the 90 degree turn or switchback it will leave the line. The bot then needs to know what to do. It could stop and search for the line but the bot may end up finding the wrong line depending on the track and how it was programmed. In addition, we need to differentiate between a gap and a sharp turn. If it's a gap we need to keep going. If not, turn in the right direction.

That's where the outermost sensors come in to play. If the bot lost the line because of a sharp turn one of those sensors has recently detected the line off to the side. We'll use that to decide what to do.

My code has two variables, lastleft and lastright. These are 32 bit integers and represent the number of mainline cycles that has gone by since the last time the left or right outer sensor was tickled. Whenever getsensres is called these outer sensors are checked and if they see a line they get zero. If they don't, they are incremented until they reach the timeout limit. When the bot leaves the line these values are checked. If they are below the timeout limit then the bot thinks it has encountered a very sharp turn and swivels on spot in that direction to try and pick up the line. The timeout is very important. The bot must also deal with gaps and the timeout is used to differentiate between them and sharp turns.

The amount of time that tells if if we lost the line because of a gap or switchback is determined by the maxlast array. We compare the lastleft or lastright value against this. If it's less, then turn. If not, it's a gap. The reason this is an array is because we can choose the bot's speed. The faster the bot is going then the shorter we want the timeouts to be. It's an array of two because we have two possible speeds determined by the jumper I described above. The topspeed variable stores this information.

When the bot decides that the reason it has lost the line is because of a switchback or 90 degree turn it will slam on the brakes by going reverse for a short time the length of which is determined, again, by our speed and the stoptime array. It will then turn on the spot in the specified direction until one or more of the three middle sensors see something and return to normal line following.

Because we can't measure the speed of the wheels (yet) these timeouts and whatnot will depend a lot on the charge left in the batteries. I typically pick values that work well when the bot has been recently charged but after running for a few minutes. Nicads and nimh batteries have a very flat discharge curve shortly after they start discharging. This makes them easy to predict.

One of the most challenging things I found I had to deal with into making the sumovore into a killer line-following robot was the distance between the line sensors and the way my code works. Because the sensors are fairly far apart it's possible for them to lose sight of the line even while the bot is over top of it. If that happens and the outer sensors were recently tickled it will think it has lost the line and needs to make a sharp turn. It will then slam on the brakes, turn for a very short distance and pick up the line again. This can manifest itself as jerky movements shorty after the bot moves past a spot where the line crosses over itself. That's why I have the linetimeout variable. If we lose the line for a short period of time then do nothing and hope for the best. It's not infallible, though. A better way to mitigate the effect would be if the sensors were a little closer together. Maybe later I will try some hacks out that we can use to help us out here.

Currently, my sumovore with this version of the code will solve my copy of the WCRG "Integrator" track with (so far) 100% reliability (i.e. no user intervention at all) in 27 seconds. That would gather 311 points assuming no points for the bonus given for a scratchbuilt robot. If it does as well on the other tracks this bot would gather over 1200 points.



This picture is a 30 second exposure of the sumovore solving a line-following track. The pink streaks are the LEDS on the top and the power LED. They look sort of pinkish because of the white balancing my new digital camera was doing. Because I wanted the LED streaks to be as bright as possible I had the apeture wide open and the room dimly illuminated by a little battery powered flourescent light.

Enjoy the code and good luck on your next competition. Come back later. I intend to expand my sumorvore by adding more sensors, a way to get more user input and add an alphanumeric LCD display. Also, It's likely I will modify the code once in a while as I find improvements or decide to incorporate a suggestion from someone who has tried it out. I will keep a version number in the code that you can compare against the one you have. If it's different then it (probably) has gotten better!

Link to Site: http://members.shaw.ca/climber/solarline.html

CODE

// Line following software for Solarbotics sumovore with Atmel brainboard.

/*

Craig Limber, July 2004.

version 1.0

permission is given to copy and modify this program all you want.
Just remember where it came from!

This code is based on the software I used for my gunnagitcha
line-following bot I compete with at the western canadian robot games.

For this version of the software I wanted to see how accurately I could
make it run on my personal line-following torture tracks while keeping
the robot hardware as stock as possible.

*/

#include <inttypes.h>
#include <avr/io.h>
#include <avr/sfr_defs.h>

enum {LINE4, LINE3, LINE2, LINE1, LINE0};
     
enum { FORWARD,    // 0
      STOP,       // 1
      NORMALLEFT, // 2
      SNAPLEFT,   // 3
      NORMALRIGHT,// 4
      SNAPRIGHT,  // 5
      BACKUP      // 6
    };

uint32_t maxlast[2] = {900, 700},
        stoptime[2] = {600, 700},
        linetimeout,
        linelost[2] = {40, 30};
int32_t lastleft = 0, lastright = 0;

uint8_t motortab[16][2][2] =
 {   // PWM range, top speed, motor
   {{  0,   0}, {  0,   0}},
   {{ 26,  26}, { 32,  32}},
   {{ 39,  39}, { 48,  48}},
   {{ 52,  52}, { 64,  64}},
   {{ 65,  65}, { 80,  80}},
   {{ 78,  78}, { 96,  96}},
   {{ 91,  91}, {112, 112}},
   {{104, 104}, {128, 128}},
   {{117, 117}, {144, 144}},
   {{130, 130}, {160, 160}},
   {{143, 143}, {176, 176}},
   {{156, 156}, {192, 192}},
   {{169, 169}, {208, 208}},
   {{182, 182}, {224, 224}},
   {{195, 195}, {240, 240}},
   {{208, 208}, {255, 255}}
 },
 topspeed;  // current speed, 0 = slow, 1 = fast

//-------------------------------------------------------------------------
void init(void)
{
/*
                   sumovore atmel brainboard pinout
                  +--------------------------------+
                  |  1 pc6 reset   adc5/SCL pc5 28 | no connect (NOT FOR LONG)
          IR left |  2 pd0 rxd     adc4/SDA pc4 27 | line left       LINE4
         IR right |  3 pd1 txd         adc3 pc3 26 | line left cent  LINE3
             LED4 |  4 pd2 INT0        adc2 pc2 25 | line center     LINE2
             LED3 |  5 pd3 INT1        adc1 pc1 24 | line right cent LINE1
             LED2 |  6 pd4 XCK/T0      adc0 pc0 23 | line right      LINE0
                  |  7 VCC                  GND 22 |
                  |  8 GND                 AREF 21 |
                  |  9 pb6 XTAL1           AVCC 20 |
                  | 10 pb7 XTAL2        SCK pb5 19 | left motor direction
             LED1 | 11 pd5 T1          MISO pb4 18 | right motor direction
             LED0 | 12 pd6 AIN0    MOSI/OC2 pb3 17 | servo1
          servo 2 | 13 pd7 AIN1    !SS/OC1B pb2 16 | left motor PWM
    servo 3/speed | 14 pb0 ICP         OC1A pb1 15 | right motor PWM
                  +--------------------------------+
*/

 // set up ports as outputs
 DDRB = _BV(DDB1)   // right motor PWM
      | _BV(DDB2)   // left motor PWM
      | _BV(DDB3)   // servo 1
      | _BV(DDB4)   // right motor direction
      | _BV(DDB5);  // left motor direction
 DDRD = _BV(DDD2)   // LED 4
      | _BV(DDD3)   // LED 3
      | _BV(DDD4)   // LED 2
      | _BV(DDD5)   // LED 1
      | _BV(DDD6);  // LED 0

 // turn on pullups
 sbi(PORTB, PB0);   // servo header 3 - jumper on = fast, off = slow

 // set up PB0 and PB1 with 8bit PWM
 TCCR1A = _BV(WGM10)    // 8 bit, phase correct PWM
        | _BV(COM1A1)
        | _BV(COM1B1);
 TCCR1B = _BV(CS10)     // no prescale
        | _BV(WGM12);  // fast PWM
}

//-------------------------------------------------------------------------
uint8_t getsensor(uint8_t sensor)
//
// this here function gets what the sensor is reading and returns a 1
// if it is getting tickled, 0 if not
//
{
 switch(sensor)
 {
   case LINE0: return(PINC & _BV(PINC4)); break;
   case LINE1: return(PINC & _BV(PINC3)); break;
   case LINE2: return(PINC & _BV(PINC2)); break;
   case LINE3: return(PINC & _BV(PINC1)); break;
   case LINE4: return(PINC & _BV(PINC0)); break;
 }
 return(0);
}

//-------------------------------------------------------------------------
uint8_t getsensres(void)
//
// this here function returns a 4 bit value that represents what the line
// sensors see. Also sets what the LEDs are seeing.  
{
 uint8_t result;

 if (PINB & _BV(PINB0))
   topspeed = 0;
 else
   topspeed = 1;

 result = 0;
 if (getsensor(LINE4))
 {
   lastleft = 0;
 }
 else
 {
   if (lastleft < maxlast[topspeed])
   {
     sbi(PORTD, PD2);
     lastleft++;
   }
   else
     cbi(PORTD, PD2);
 }
 if (getsensor(LINE0))
 {
   lastright = 0;
 }
 else
 {
   if (lastright < maxlast[topspeed])
   {
     sbi(PORTD, PD6);
     lastright++;
   }
   else
     cbi(PORTD, PD6);
 }
 if (getsensor(LINE1))
 {
   result |= 0x4;
   sbi(PORTD, PD5);
 }
 else
   cbi(PORTD, PD5);
 if (getsensor(LINE2))
 {
   result |= 0x2;
   sbi(PORTD, PD4);
 }
 else
   cbi(PORTD, PD4);
 if (getsensor(LINE3))
 {
   result |= 0x1;
   sbi(PORTD, PD3);
 }
 else
   cbi(PORTD, PD3);
 return(result);
}

//-------------------------------------------------------------------------
void setmotorspeeds(uint8_t left, uint8_t leftdir,
                   uint8_t right, uint8_t rightdir, uint8_t topspeed)
{
 OCR1B = motortab[left][topspeed][0];
 OCR1A = motortab[right][topspeed][1];
 if (leftdir == FORWARD)
   sbi(PORTB, PB5);
 else
   cbi(PORTB, PB5);
 if (rightdir == FORWARD)
   sbi(PORTB, PB4);
 else
   cbi(PORTB, PB4);
}

//-------------------------------------------------------------------------
void go(uint8_t dir)
{
 switch (dir)
 {
   case STOP:
     setmotorspeeds( 0, FORWARD,  0, FORWARD, topspeed);
     break;
   case FORWARD:
     setmotorspeeds(15, FORWARD, 15, FORWARD, topspeed);
     break;
   case NORMALLEFT:
     setmotorspeeds( 4, BACKUP, 15, FORWARD, topspeed);
     break;
   case SNAPLEFT:
     setmotorspeeds(14, FORWARD, 14, BACKUP, topspeed);
     break;
   case NORMALRIGHT:
     setmotorspeeds(15, FORWARD,  4, BACKUP, topspeed);
     break;
   case SNAPRIGHT:
     setmotorspeeds(14, BACKUP, 14, FORWARD, topspeed);
     break;
   case BACKUP:
     setmotorspeeds(15, BACKUP, 15, BACKUP, 3);
     break;
 }
}

//-------------------------------------------------------------------------
int main (void)
{
 uint32_t x, loop;
 uint8_t sensres, last;
 
 init();
 go(STOP);
 for (loop=0; loop<3000000; loop++) x--;  // let human oppressor remove finger
 go(FORWARD);
 last = FORWARD;
 while (1)
 {
   sensres = getsensres();
   switch(sensres)
   {
     case 0x0:   // lost site of the line
       if (linetimeout < linelost[topspeed])   // wait until line really gone
       {
         linetimeout++;
         break;
       }
       if ((lastleft < maxlast[topspeed]) && (lastleft < lastright))
       {
         go(BACKUP);
         for (loop=0; loop<stoptime[topspeed]; loop++) x--;
         go(SNAPLEFT);
         do
           sensres = getsensres();     // turn til line reappears
         while (!(sensres));
         //lastleft = 0;
         last = SNAPLEFT;
       }
       else
         if ((lastright < maxlast[topspeed]))
         {
           go(BACKUP);
           for (loop=0; loop < stoptime[topspeed]; loop++) x--;
           go(SNAPRIGHT);
           do
             sensres = getsensres();   // turn til line reappears
           while (!(sensres));
           //lastright = 0;
           last = SNAPRIGHT;
         }
         else
         {
           go(FORWARD);   // line lost but not off to side, go straight
           last = FORWARD;
         }
       break;
     case 0x1:
       go(NORMALRIGHT);
       last = NORMALRIGHT;
       break;
     case 0x2: // if middle sensor sees something go straight regardless
     case 0x3:
     case 0x6:
     case 0x7:
       go(FORWARD);    
       last = FORWARD;
       break;
     case 0x4: go(NORMALLEFT);  last = NORMALLEFT; break;
     default:  go(last); break;
   }
 }
}




Update: Sun April 10, 2005

The 1st place winner of the line-following competition at the 2004 Eastern Canadian Robot games was Nicholas Barker with "Truffle Pig." His bot's software was based on the above code. Congratulations Nicholas!


--------------------------------------------------------------------------------


Update: Mon May 23, 2005

The 1st place winner of the line-following competition at the 2005 Western Canadian Robot games was, again, Nicholas Barker with "Truffle Pig." He cleaned my bot's clock by a wide margin. Congrats again Nicholas! And, yup, it's time to update my bot.


--------------------------------------------------------------------------------


Update: Thu June 16, 2005

The 1st place winner of the advanced line follower at the Vancouver Robotics Games on saturday June 11 was Clinton Blackmore's "Circuit Breaker;" an unmodified Sumovore with the code from above. Way to go Clinton!



Guest- 06-24-2006
a simple line follower
http://www.geocities.com/njbibin/robotics/linefollower.html

Forumer™ is Voted #1 Free Forum Hosting provider
Build your own community today with the largest message board hosting company.