Dr. Dobb's Journal October, 2004
While robots are often described as "autonomous devices," this simple description fails to convey the complexity associated with them. At some level, robots must be aware ofand interact withtheir surroundings. They may also need to process sensory data to determine a course of action such as (with mobile robots) avoiding obstacles. Microcontrollers can be used to gather and process this data and, if powerful enough, the same microcontroller can be used to control the "thought process" and robotic propulsion mechanisms.
Sensory data comes in many forms. Humans sense such things as light, sound, smell, and touch. Robots are able to use senses that we ordinary, biological lifeforms simply don't have or that are beyond the range of our normal sensesinfrared and ultrasonics, for instance. With the application of ultrasonics (sonar) becoming increasingly widespread, the application of unmanned vehicles (aerial, surface, and oceanic) is also expanding, particularly in the exploratory and military/defense arena. In this article, I use an off-the-shelf robot chassis, a single-board computer, and Small-C to build a mobile autonomous robot that mimics the design and implementation of industrial, real-world autonomous systems.
The TankBot Servo Kit from Budget Robotics (http://www.budgetrobotics.com/shop/index.php?shop=1&cat=51) has everything you need to build a servo-motor robotic tracked vehicle. It uses two radio-controlled (R/C) servo motors (44 oz-in torque) that are already modified for continuous rotation, therefore, require no disassembly or soldering. The assembled TankBot Servo measures 7-inches long, 4.75-inches wide, and 3.5-inches high, and is available in a variety of colors. Assembly is straightforward. Because there are no glues or tape involved, the TankBot Servo can be fully disassembled, too.
The chassis includes a precision pre-drilled/precut plastic base with a second deck for a microcontroller or other electronics. The kit includes a Tamiya tank tread kit, which comes with flexible but strong rubber treads, drive sprockets, and idler wheels, and illustrated assembly instructions. All construction hardware (nuts, bolts, risers, and the like) is included. The whole kit retails for less than $55.00.
An optional extra for the TankBot Servo is the Servo Turret. This kit includes all the mechanical parts to make a compact and affordable servo-controlled rotating turret for the Devantech SRF0x family of ultrasonic ranging sensors (http://www.robot-electronics.co.uk). The assembly can be mounted using the supplied nylon standoffs, or with a heavy-duty 3M Dual Lock reclosable fastener (also included). The turret works with the Devantech SRF04 and SRF08 sensors and is constructed from quality custom-made ABS plastic parts (it won't "ring" at high frequencies like metal brackets can). Rubber grommet shock mounts attach a sensor board to the turret, which helps reduce microphonic interference. The kit contains a standard-size Cirrus brand servo with a Futaba-style splined hub, with a universal 3-pin plug (can be used with Hitec/JR, Futaba-J, and Airtronics-Z-style connectors), all assembly hardware and illustrated assembly instructions, with a list of sensor sources. Like the TankBot Servo kit, the Servo Turret can be fully disassembled, and the Turret kit retails for less than $19.00. The Devantech SRF04 ultrasonic sensors retails for about $33.00, and the SRF08 about $50.00, depending on the source.
The bottom deck of the robot chassis is large enough to accommodate a 4-AA battery pack, plus a small circuit board or 9-volt battery. The top deck measures 4-5/8×2-7/8 inches, which is large enough for a variety of popular microcontrollers. These decks are separated by four 1-inch polypropylene-reinforced risers. An optional third deck is available separately for mounting additional components.
As with all robots, you need to consider power requirements, depending upon how many extra parts you'll be attaching to your robot. If you're underpowered, the current drain can have the undesirable effect of resetting your microcontroller. In addition, if you employ items such as the Servo Turret in your design, you need to keep in mind that this is a moving partand as such, will require flexible power and control cabling for whatever sensor you mount on it. Castle Creations (http://www.castlecreations.com/) provides ultra flexible silicone cablingsold in sensible lengthswhich is soldering-iron safe. Finally, keep in mind that some sensor boards don't come with headers, so you may need to perform a small amount of direct cable soldering oras I did with the SRF04simply solder a header to the board.
Because I wanted to experiment with ultrasonics, I opted for the TankBot Servo Kit plus the Servo Turret andfor maximum flexibilitythe third deck; see Figure 1. This gave me plenty of space to fit additional battery packs and any other sensors I might want to play with once the initial project was complete.
For the "brains" of the system, I selected New Micros's MiniPod (DSP56F803), which is based on Motorola's flashable DSP56F80x microcontroller (http://www.newmicros.com/). While any of New Micros's single-board computers would have worked, the MiniPod has mounting points that are directly aligned with the TankBot's riser drill points, and leaves you with lots of free space on the top deck. Moreover, the MiniPod can be coded using IsoMax (New Micros's real-time programming language), MetroWerks C, Small-C, or assembly language. (A JTAG cable is not required for IsoMax.)
The three servosone for each track and one for the turretrequire Pulse Width Modulator (PWM) controls, and the sonar requires a line for the trigger and another for the echo. I used PWMA0 and PWMA1 for the left and right tracks, and PWMA2 for the turret. TC0 and TC1 were used as the trigger and echo signals because they are conveniently attached to one of the MiniPod's timer moduleswhich makes it easy to time the gap between signal and echo and, hence, calculate distance. In addition, I used the MiniPod's onboard LEDs to indicate the state of the software (for example the red LED indicates an object to the right of the robot is getting closer). The sonar sensor has an approximate 30-degree beam angle, for a total of 60 degrees. Dividing 180 by 60 gives 3, meaning that about the best resolution possible was going to be one of three sonar readingsleft, forward, and right. Good enough for a robot to find its way around a room without bumping into anything.
I wrote the control code in Small-C (available at http://www.ddj.com/ and http://petegray.newmicros.com/). Listing One is the main program. The variables ldist, rdist, and fdist are the distance readings from the sonar to the left, right, and front of the robot. Element [0] is the current reading, and element [1] the previous reading.
do_sonar() pings the sonar, turn() executes a 90 or 180 degree turn (left or right), adjust() modifies the speed of the track servos to make a slight course correction (using the global variables, lspeed and rspeed), and do_tracks() feeds these speed variables into the PWM module.
Basically, the program takes a sonar reading to the left, front, and right of the robot, and calculates the distance. It then compares the distances to the previous readings and redirects the robot accordingly. If the software determines that the robot is getting closer to an object on a particular side, it will turn the robot away from the object. If the software decides that an object is too close to the robot, it performs a 90 or 180 degree turn, accordingly. The adjust() routine increases (or decreases) the speed of just one of the tracks, so the robot actually steers rather than "turning on the spot." "Full-speed ahead" mode is reestablished once the robot is clear of obstacles.
Notice that "full-speed ahead" is achieved by setting lspeed and rspeed to SMAX and SMIN, respectively. This may look totally wrong, but you have to remember that the left and right servos are physically mounted at 180 degrees to each other. Hence, if both servos were fed the SMAX value, the robot would simply rotate on the spot.
Listing Two reveals the secrets of track-speed adjustment. SINC is the fixed adjustment delta, which is applied to the appropriate track's servo. Note again in this piece of code that the signing of the variables looks incorrect due to the physical servo orientation. Once adjusted, the routine checks to ensure that the PWM ranges haven't been exceeded and modifies their values, if required.
Listing Three presents the turn() routine, which turns the robot, left or right, through 90 or 180 degrees. The first thing it does is set the lspeed and rspeed according to the turn direction. It then calls turn90d()twice for a 180 degree turnto perform the actual turn. Next, it adjusts the rdist, ldist, and fdist "previous" values, which have changed due to the new orientation of the robot. For example, a 90 degree left turn makes what used to be "straight ahead" become "right." Accordingly, the value in fdist[1] needs to be placed in rdist[1]. Finally, it sets the robot to "full-speed ahead."
The do_sonar() routine (Listing Four) saves the "previous" distance readings, sets the PWM value for the servo turret, waits for the servo to move by calling wfs(), pings the sonar, and records the reading. It does this three times, and then sets the lmcloser and rmcloser flags (left- or right- moves closer), which are used in the main program. The wfs() function is a simple predetermined loop because I chose not to employ a servo feedback mechanism. For anyone wishing to employ such a mechanism, quadrature encoders could be utilized, although a homemade assembly would be required to align them withand have them driven fromthe tracks. The software controls the turret servo so that it starts and ends in the TMID (forward) position, ready for the next "scan" of the robot's environment.
Pinging the sonar turned out to be more complex, although the underlying mechanism itself is fairly straightforward. To ping the sonar, you simply set an input signal "high." When the sonar detects the echo, it sets the echo line. From the time taken between signal and echo, you calculate the distance. Listing Five shows an individual "ping." As a precaution, I coded this routine to ping each direction four times and take the average reading. As an additional precaution, I automatically reping if, for whatever reason, the sonar doesn't respond. TMRCAP1 is the timer capture valuethe timer is started when the trigger signal is sent and stops when the echo is received. Recall that the timer pings are tied directly to the trigger and echo lines of the sonar.
Putting the TankBot together was fun and easy. The materials used are durable and the mounting area extensive, particularly if you add the optional third deck. Although I used the Servo Turret, there's no reason different kinds of sensors (IR, for instance) couldn't be used, and the MiniPod certainly has the capability to interface with all of them, as well as controlling the track servos. Moreover, Small-C provides an easy (and free) embedded software development tool. In addition, the Pod series supports the inherently multitasking IsoMax as well as assembler. All of the software is available either preloaded on the microcontroller or available for download from New Micros.
DDJ
ldist[0] = LMIN;
rdist[0] = RMIN;
fdist[0] = FMIN;
do_sonar(); // take initial sonar reading
lspeed = SMAX; // default to "full speed ahead"
rspeed = SMIN;
while (1) { // loop forever
do_sonar();
// object too close in front ?
if (fdist[0] < FMIN) {
if (rdist[0] <= ldist[0])
turn (LEFT90);
else
turn (RIGHT90);
do_sonar();
}
else {
// too close, both sides ?
if (rdist[0] < RMIN && ldist[0] < LMIN) {
if (rdist[0] <= ldist[0])
turn (LEFT180);
else
turn (RIGHT180);
do_sonar();
}
else {
// too close, either side ?
if (rdist[0] < RMIN || ldist[0] < LMIN) {
// object too close to right ?
if (rdist[0] < RMIN)
adjust (LEFT);
else
// no, object must be too close to left
adjust (RIGHT);
}
else {
// no objects too close, but are any getting closer ?
if (rmcloser == 1 && lmcloser == 0)
// closer to right
adjust (LEFT);
else
if (lmcloser == 1 && rmcloser == 0)
// closer to left
adjust (RIGHT);
else {
// no object too close, and none getting closer
lspeed = SMAX;
rspeed = SMIN;
}
}
}
}
do_tracks();
// set LEDs
// green = left closer. red = right closer. both = forward closer.
val = 0;
if (rdist[0] < ldist[0] && rdist[0] < fdist[0]) val = 0x0004;
if (ldist[0] < rdist[0] && ldist[0] < fdist[0]) val = 0x0008;
if (fdist[0] < ldist[0] && fdist[0] < rdist[0]) val = 0x000C;
*PEDR = val;
}
Back to article// slight turn - decelerate inner track if possible, else accelerate outer
adjust (direct)
int direct;
{
if (direct == RIGHT) {
if (rspeed == SMIN)
rspeed += SINC;
else
lspeed += SINC;
}
if (direct == LEFT) {
if (rspeed == SMIN)
lspeed -= SINC;
else
rspeed -= SINC;
}
if (lspeed < SMIN) lspeed = SMIN;
if (lspeed > SMAX) lspeed = SMAX;
if (rspeed < SMIN) rspeed = SMIN;
if (rspeed > SMAX) rspeed = SMAX;
}
Back to articleturn (direct)
int direct;
{
int t;
// set servo speeds ready for rotation
if (direct==LEFT90 || direct==LEFT180) {
lspeed = SMIN;
rspeed = SMIN;
}
if (direct==RIGHT90 || direct==RIGHT180) {
lspeed = SMAX;
rspeed = SMAX;
}
if (direct==LEFT90 || direct==LEFT180) turn90d (LEFT);
if (direct==LEFT180) turn90d (LEFT);
if (direct==RIGHT90 || direct==RIGHT180) turn90d (RIGHT);
if (direct==RIGHT180) turn90d (RIGHT);
// juggle the distance arrays
if (direct==LEFT90) {
rdist[1] = fdist[1];
fdist[1] = ldist[1];
ldist[1] = 100;
}
if (direct==RIGHT90) {
ldist[1] = fdist[1];
fdist[1] = rdist[1];
rdist[1] = 100;
}
if (direct==RIGHT180 || direct==LEFT180) {
t = rdist[1];
rdist[1] = ldist[1];
ldist[1] = t;
fdist[1] = 100;
}
// default to "full speed ahead"
lspeed = SMAX;
rspeed = SMIN;
}
Back to articledo_sonar()
{
ldist[1] = ldist[0]; // save previous readings
rdist[1] = rdist[0];
fdist[1] = fdist[0];
*E08 = TLEFT; // rotate left
do_pwm();
wfs(); // wait for servo
ping();
ldist[0] = pingres; // save reading (current)
*E08 = TMID; // rotate forward
do_pwm();
wfs(); // wait for servo
ping();
fdist[0] = pingres; // save reading (current)
*E08 = TRIGHT; // rotate right
do_pwm();
wfs(); // wait for servo
ping();
rdist[0] = pingres; // save reading (current)
*E08 = TMID; // rotate forward, ready to start again
do_pwm();
wfs(); // wait for servo
if (ldist[0]<ldist[1]) // set lmcloser
lmcloser = 1;
else
lmcloser = 0;
if (rdist[0]<rdist[1]) // set rmcloser
rmcloser = 1;
else
rmcloser = 0;
}
Back to article tmrcnt=0;
do {
reset_timers();
while ((*TMRSCR1 & 0x0800) != 0x0800) {
tmrcnt++;
if (tmrcnt==2000) {
tmrcnt = 0;
reset_timers();
}
}
} while ((*TMRSCR1 & 0x0800) != 0x0800);
p1 = *TMRCAP1; // get capture value
p1 /= 183; // convert to inches
Back to article