Encoder Front Page
SRS Home | Front Page | Monthly Issue | Index
Google
Search WWW Search seattlerobotics.org

My first robot:

A Bot Board Plus line Following Robot

By Jeff Spencer, jeffsp@nwlink.com

jeffsp1.jpg (19722 bytes)

First of all I’ll give some history of how I came to build a line following robot. Recently I became interested in robotics and attended my first SRS meeting. Fortunately the meeting I picked happened to coincide with the Fire Fighting Robot contest. As a result of the competition I became even more enthused about robots and various methods of determining the exact position of a robot within a home. And since CCD cameras are getting cheaper I spent a lot of time looking into very high resolution robot vision solutions. Incidentally I hope to publish what I learned in the Encoder in the future. Anyhow, after a month of reading and web based research I attended my second SRS meeting. After the meeting I joined a lot of the SRS members at the local Pizza joint. During the ensuing conversation some of the SRS members convinced me that starting with a simple robot made sense. In fact, they specifically suggested that I target the upcoming line maze contest using a Bot Board Plus for the CPU (available from http://www.nwlink.com/~kevinro/products.html ) , a Marvin Slyder Base (available from http://www.sinerobotics.com/ ), and write the program in either Sbasic (available from Karl Lunt’s home page www.seanet.com/~karllunt ) or assembly language. At the time, I thought I would do the programming in C or assembly language (I’m a software engineer by day) which is probably why they suggested the Bot Board Plus, since the 2K EEPROM could get cramped for doing an Sbasic line maze robot.

In the end, I ordered the components above and decided to use Sbasic since it came with the Bot Board Plus and was clearly the path of least resistance. Also, writing assembly language is a very slow process because the instructions are so primitive compared to a high level language.

So once I finally decided what I was going to buy it was time to order everything.

- I ordered the Bot Board Plus starter kit, but if I had to do it again I’d order the full assembled version for my first bot (reasons below).

- I ordered the Futuba S148 servos because I found multiple references to converting these to continuous rotation and Karl’s book provides a description of how to add an encoder to this (I’ve yet to add the encoders).

- And since I’m really serious about this, I also ordered all of the Kevin Ross’s favorite parts from Digi-Key. To find his favorite parts see the July 1999 Encoder. If you’re just getting started you’ll need a Molex Universal Crimp Tool to attach wires to the connectors that come with the Bot Board Plus Starter Kit. This is Digi-Key part number WM9999-ND ($32.59). Initially I had trouble finding the correct Molex KK connectors. But some quick e-mail to Kevin solved that problem. The 4 circuit terminal housing is WM2002-ND, the tin plated version of the 4 circuit terminals is WM4202-ND and the crimp on connectors (that go in the housing) with gold contacts is WM2312-ND. (I actually intended to buy all gold plated connectors but accidentally bought tin headers). Tin is probably just fine, but since I’m just getting started and didn’t want any extra complications due to a bad connection I spent the extra money.

- I also order some photo detectors, Digi-Key part #L14G1 which are QT optoelectronics part #L14G1 more about them later.

- What I didn’t order but should have is some 10 pin ribbon cable sockets and 10 pin ribbon cable to connect to the digital and analog I/O headers of the Bot Board Plus

I’ve purchased several other items since then, but I believe this covers the bulk of the items I needed.

The assembling of the BotBoard Plus and the serial cable for the Bot Board Plus went smoothly. However, when I was done the serial cable didn’t work. And since I haven’t taken the time to get it working I can’t tell you why. Since I know there aren’t any cold solder joints (I re-flowed everything) I suspect either a solder bridge or I overheated a part. Anyhow, I was stuck cold at this point. Primarily due to lack of tools. I couldn’t find my multi-meter, I didn’t have an oscilloscope and didn’t have an extra Bot-Board or download cable to isolate the problem. Out of frustration I solved the problem with the shotgun approach. In other words I resolved all of those situations at the same time. I ordered an oscilloscope, a much better multi-meter than the radio shack model I couldn’t find and ordered a fully assembled Bot Board Plus and cable. While it was very frustrating waiting for all of this to arrive.

You can imagine my glee once all those orders arrived. Using the new serial/signal cable I quickly determined that I had two completely functional Bot Board Plus’s to work with. Needless to say, programming my new bot controller was much more interesting than debugging a hardware problem in a serial cable. Which explains why I haven’t fixed the original serial cable yet.

Since serial output looked very simple using Sbasic. I decided to write a "Hello There" program first to test my board. Unfortunately after several hours (late at night) it wasn’t working. So I gave in and did something simpler. I wired a LED through a 150 ohm resistor to an output on Port-C. Since at that point I simply wanted to get ANYTHING working I wrote a quick loop to blink all the outputs on Port C. It worked like a charm. So I was back to trying to get serial output working.

I decided it was time to work through the serial I/O one step at a time. So the first step was to read the 68HC11 manual and figure out what the correct initialization sequence was, even thought every example I found used the same initialization sequence I was. Which was:

pokeb baud, $30

pokeb sccr2, $0c

In the end the problem was that my HC11 also required that a zero be written to sccr1. So once I changed the initialization sequence to the following code I could finally print to the serial port and use Hyperterm to see what was happening within my code. I can’t help but wonder if everyone hits this issue and has to work through it.

pokeb baud, $30

pokeb sccr1, $00

pokeb sccr2, $0c

OK, now that I had a working microcontroller it was time to learn how to read the analog ports. Using some example code of Karl’s I very quickly had the first four channels working. However, I couldn’t read the 2nd set of four channels until Kevin Ross helped me out after I sent a plea to the SRS mailing list (thanks Kevin).

The Line Sensor Array

I decided to use Karl Lunt’s line sensor array design from his book Build Your Own Robot! Which if you haven’t bought yet, you really need to. Page 180 has the schematic that I used for the sensors. Unfortunately, I couldn’t find the surplus photo transistors that he used so I had to modify the resistor values to match the photo transistors I’m using. With Opto Electronics L14G1 (Digi-Key part number L14G1) I found that a 1.2K resistor gives a good range of values for measuring a black line on white paper. Note that per Karl’s advice I’m using RED super-brite LED’s driven by a 5+ V power supply through a 150 ohm resister. Karl recommends not using Infrared Light because some paper is transparent to infrared light and the sensor is likely to detect features underneath the paper. And while following an expansion crack in a concrete floor through paper is pretty cool, it’s not a good way to win a line following or maze solving contest.

jeffsp2.jpg (14708 bytes)

Click here to watch a Real Player movie of this robot in action (127kb for 56k modem)

I determined the best resistor value for use with the photo transistor by experimentally driving it through a 5K potentiometer taken from the Futuba S148 servos. While 1.2K seemed like the best value to use. I found that I had more 1K resistors than 1.2K, so I went with the 1K resistors instead. And they seem to be working fine. While I haven’t tested this, if you like you can use Opto Electronics L14G2 instead of L14G1. The G2 model simply gates half as much current (3 milli-amps instead of 6 milli-amps) as the L14G1 part I used. So in theory a 2.2K resistor used with the L14G2 will work just as well, but with half the current draw.

jeffsp3.jpg (22388 bytes)

While I’d like to go on to describe the rest of my experience in detail. I don’t have anymore time to dedicate to this article. So at this point I’ll simply describe my current program and include the Sbasic code. I would like to mention that the BotBoard Plus schematic I have is wrong. And if you’re working off of it be aware that the port labeled Servo 1 is in fact PA6 and not PA3 as the schematic states. Likewise Servo 2 is in fact PA5. Presumably, Servo 3 is PA4 and Servo 4 is PA3 but I haven’t verified this. Also the schematic incorrectly shows pin 1 on the servos as the signal line. In fact pin 1 (indicated by the square pad on the circuit board) is the ground line and pin 3 is the signal line. If you look at the traces on the circuit board it should be fairly obvious as pin 1 clearly has a trace running to pin 1 on all the servo headers. I lost a lot of sleep figuring all that out. Hopefully I can save some people some time by mentioning it here. As far as I can tell Port A’s 10 pin header is correct on the schematic.

This robot is intended to become a line-maze robot. But at this point it’s simply a line following robot. And a very simple one at that. The robot starts off by initializing the serial port, the D to A converter and the pulse width modulation control for the servos. It then spins in place multiple times calibrating the sensors to the current environment. Then it turns until it finds the line and starts following it by alternately turning the left or right wheel. I’ve put lots of comments in the code to help, so I’ll let the code explain the rest.

Editors note: To download the code as a file, click here.
include  "regs11.lib"
declare i
declare  n
declare  t
declare  x
declare z
declare y
declare n2
declare Fob
declare FobAdjust
declare Tmp
' Ground sensor values
declare LeftCenterG
declare RightG
declare LeftG
declare RightCenterG
declare TimeIt
declare wait
declare RollIt
declare Number2
declare TimeCount
declare TimeCountH
declare iTmp
declare Analog(8)
declare SenseMax(8)
declare SenseMin(8)
declare SenseAverage(8)
declare SenseTotalL(8)
declare SenseTotalH(8)
declare SenseThreshold(8)
declare SenseDiff(8)
const LeftStop = $9AA
const LeftFF   = $2000
const LeftFR   = $200
const RightStop =$9EB
const RightFF   =$200
const RightFR   =$2000
const LeftSpeed = TOC2
const RightSpeed = TOC3
const L = 2
const LC = 6
const RC = 3
const R = 7
wait = 0
TimeIt = 0
RollIt = 0
y = 0
n = 14
z = 1
n2 = 0
Number2 = 50
'
'  The RTI interrupt service routine
'
interrupt $fff0 
'RTI Interrupt
  if peekb(tflg2) and %01000000 <> 0
    if wait <> 0
      wait = wait - 1
    endif
    TimeIt = TimeIt + 1
    pokeb  tflg2, %01000000
  endif
' keep track of PWM timer roll over
  if peekb(tflg2) and %10000000 <> 0
    RollIt = RollIt + 1
    pokeb  tflg2, %10000000
  endif
' I don't think this is used at the moment
' but in case it is I'll leave it in for the Encoder article
  if peekb(tflg1) and %01000000 <> 0
    Number2 = Number2 + 1
    pokeb  tflg1, %01000000
  endif
end
'This subroutine blinks an LED attached to any of the 
'output lines on port c. It will be removed at some time in the future
'currently it's never called. To call it simply add the line
'call Alive 
Alive:
y = 0
for n2 = 0 to 10
    for n = 0 to 1
        for x = 0 to 6300
        next 
    next
 
    ' Alternate between all on and all off
    if y = 0    
 y = $ff
    else
 y = 0
    endif
    pokeb portc, y 'Change port C to either all low or all high
next
pokeb portc, 0
return
'This subroutine reads all of the Analog To Digital channels when it's called
'It stores the results in the array "Analog".
'It also stores the difference from the average value which was calculated
'earlier. This difference value is meaningless until after the Calibrate 
'subroutine is called. 
sense:
pokeb adctl, $10    ' multi-chnl, 1-4
waituntil adctl, $80    ' loop until conversion complete
Analog(0) = peekb(adr1)    
Analog(1) = peekb(adr2)    
Analog(2) = peekb(adr3)
Analog(3) = peekb(adr4)                       ' Analog 4
pokeb adctl, $14                                ' multi-chnl, 5-8
waituntil adctl, $80
Analog(4) = peekb(adr1)    
Analog(5) = peekb(adr2)    
Analog(6) = peekb(adr3)    ' Analog 7
Analog(7) = peekb(adr4)     ' Analog 8
for iTmp = 0 to 7
  SenseDiff(iTmp) = Analog(iTmp) - SenseAverage(iTmp)
next
Fob = Analog(0)
return
'Calibrate
'Does software calibration of the analog channels. The code
'runs for all the analog channels even though currently only
'4 of them have line sensors attatched. This keeps the code simple
'but if memory constraints become a problem it will need to be 
'optimized to only read the line sensors.
'It works by spinning the robot around for awhile and 
'tracking the largest and smallest readings for each sensor
'It also averages all of the readings for each sensor.
'Since SBasic doesn't have 32 bit integers it's necessary
'to calculate an average on 128 samples at a time. Then average
'each of those averages at the end of the calibration time.
'While this is inconvient, it works just fine. It is possible to
'loose up to 127 samples at the end, but on average we'll only
'loose half that many (64) and compared to the total samples
'I measured of approximately 3500 that's an insignifigant
'number of samples. 
'Since the majority of the paper doesn't have any lines on it
'the average value is very close to the value of the paper
'without a line. Providing a very good basis for determining
'if a line is present or not.
'Lastly this function calculates a sense threshold. This threshold is
'used to determin if a sensor is over a black line. It's calculated by
'picking a value exactly half way between the average and the maximum
'value for that sensor. The goal is to privide a threshold with lots
'of margin for error.
Calibrate:
  'Spin in place counter clockwise
  poke RightSpeed, RightFF
  poke LeftSpeed, LeftFR
 
  ' Initialize with values to be replaced
  for i = 0 to 7     ' Loop through all 8 analog channels
    SenseMin(i) = 255
    SenseMax(i) = 0
    SenseTotalL(i) = 0
    SenseTotalH(i) = 0
  next
  ' Calculate min and max and average values while spinning in place
  RollIt = 0
  TimeCount = 0
  TimeCountH = 0
  while RollIt < 400  'arbitrarily wait until the PWM generator has rolled over 400 times
    gosub Sense
    TimeCount = TimeCount + 1
    for i = 0 to 7
      SenseMin(i) = Min(SenseMin(i), Analog(i))
      SenseMax(i) = Max(SenseMax(i), Analog(i))
      SenseTotalL(i) = SenseTotalL(i) + Analog(i) 'Total the readings for the Low average
    next
 
    'If it's time to calculate the low average, then do it
    if TimeCount = 128 
      TimeCount = 0   'Prepare to add up another 128 samples
      TimeCountH = TimeCountH + 1 'Count the number of low averages we're adding to High average total
      for i = 0 to 7
        x = SenseTotalL(i) / 128
        SenseTotalL(i) = 0
        SenseTotalH(i) = SenseTotalH(i) + x
      next      
    endif
  wend
  ' print the results for debugging
  for i = 0 to 7
    SenseAverage(i) = SenseTotalH(i) / TimeCountH
    x = SenseAverage(i) + SenseMax(i)
    SenseThreshold(i) = x / 2
    printx "Index:"; i;"  Min:"; SenseMin(i); "   Max:"; SenseMax(i); "   Average:"; SenseAverage(i); "  Thresh:"; SenseThreshold(i)
  next 
  printx "TimeCount:"; TimeCount
  printx "TimeCountH:"; TimeCountH
  'Time to stop spinning in place
  poke RightSpeed, RightStop
  poke LeftSpeed, LeftStop
return
main:
'Initialize the serial port
pokeb  baud, $30
pokeb  sccr1, $00  'critical line that was missing from the examples I used
pokeb  sccr2, $0c
'Set Port C is all outputs and set all the pins low
pokeb  ddrc, $ff
pokeb  portc, $00
'Overflow and RTI
pokeb   tflg2, %11000000
pokeb   tmsk2, %11000000
'Initialize A/D
pokeb  option, $80  ' turn on A/D system
' Set up for PWM
pokeb OC1M,  %11111000    ' Out Cmp 1 affects PA7-PA3
pokeb OC1D, %11111000     ' Sending them high
pokeb TCTL1, %10100000 ' OC2 turns off PA6 and OC3 turns off PA5
pokeb TMSK1, %00000000 'no interrupt
pokeb TFLG1, %00000000 
'Stop the servos by sending the correct PWM as determined through experimentation
poke  TOC2, LeftStop      'Stop for left servo
poke  TOC3, RightStop
interrupts on
'Gosub Alive   'don't call the subroutine alive anymore
print "Hello There"
print
'Z never equals 35 so this code isn't used anymore. I used it while figuring out the 
'PWM values to use for stop. Fob is an analog channel that I hung a potentiometer off of. 
'The potentiometer was wired as a voltage divider to provide a number between 0-255 depending
'on it's position. 
if z = 35
  gosub Sense
  Tmp = peek(TOC2)
  FobAdjust = Fob - 8
  if FobAdjust < 0
     FobAdjust = 0
  endif
  FobAdjust = $950 + FobAdjust
  Tmp = FobAdjust
  printx "Fob: "; Fob; "  NewVal:"; Tmp
  
  poke TOC3, Tmp
 for x = 0 to 2000
 next
endif

'Calibrate the analog line sensors
GoSub Calibrate
' Spin counter clockwise
poke RightSpeed, RightFF
poke LeftSpeed, LeftFR
' Keep spinning until we find the line by watching the Right Center (RC) 
' Analog Sensor. When we find it the line should be between the left center and 
' right center sensors. At which point it's time to simply follow the line.
n = 0
while n = 0
  Gosub Sense
  if Analog(RC) > SenseThreshold(RC)
    n = 1
  endif
wend
'Follow the line by adjusting the pulse width modulation (PWM) 
'each time the PWM clock generator rolls over (starting the pulse)
RollIt = 0
while z<> 0 ' Loop forever
  if RollIt > 0  ' Wait for the PWM counter to roll over
     RollIt = 0  ' Reset PWM rollover flag for next time.
     gosub Sense  'Read the line sensors
       x = SenseDiff(RC) - SenseDiff(LC)  'Get the difference in the readings
       'Convert the difference to an absolute value
       if x < 0
          x = 0 - x
       endif
       'If the difference is small enough run both wheels forward
       'Note: This isn't working well and needs work. x is rarely, if ever under 8
       if x < 8
   poke RightSpeed, RightFF 
          poke LeftSpeed, LeftFF
       else
  'Turn which ever way is necessary to center the line.
         if SenseDiff(RC) > SenseDiff(LC)
            'too far right
     poke RightSpeed, RightStop 
            poke LeftSpeed, LeftFF
         else
     poke RightSpeed, RightFF
            poke LeftSpeed, LeftStop
         endif
       endif
  endif
wend