Andrew's Leap

Summer 1997


Robot Programming


Draft: July 7, 1997.

Back to Top of Course Notes

** Split the four sections into four different pages **

In this section you will learn to program the robots. We assume a rudimentary knowledge of C. You will learn the ins and outs of the HC11, and how to interact effectively with buttons, switches, light sensors, motors, and other devices. You will also learn the rudiments of real-time programming, where several independent processes use simple multitasking to drive the robot.

Readings:

Session 1: How to read a button

** Too deep too quickly. Forget the button, do a switch. Maybe show them a memory map for all the devices. Save debouncing a button for advanced exercise for students who might be ahead of the norm. **

** Maybe this is the time to tell them about voltages, switches, logic.**

Computers enable us to perform seemingly impossible tasks with great ease. On the other hand, simple things become complicated. For example, what could be simpler than a button? But did you know that buttons and switches bounce? In this session you will learn how to "debounce" switches. Also, you will learn the library routines for doing console IO, for reading buttons and switches, and for using the clock; you will learn how fast the computer is and how fast a human is.

First, write a straightforward program to count edges. (An edge is a transition from 0 to 1 or 1 to 0. A pulse is two edges back-to-back.) Do this by modifying the program intro.c. That way you'll get all the right header files included, and you'll get some other stuff you need. Note that every program should start with a call to leap_init and end with a call to leap_done. Here is some pseudo code for the edge counter:

	Repeat forever:
		Get desired time delay from user
		Set the edge counter to 0
		For the requested amount of time:
			If edge, increment the edge counter
		Print the number of edges

Does "repeat forever" seem like a bad idea? The easiest way to exit a program is to push the reset button!

How to get a number from the user. We have printf but not scanf, so the easiest way is to use the function getnum which reads a number from the console. The prototype is

int getnum( void )

How to work with time delays. Use the functions set_delay and check_delay. Delays are in units of "ticks". One second is 112 .5 ticks. set_delay starts a timer counting down, and check_delay returns 1 if the timer is still counting, 0 if it has timed out. The prototypes are

char set_delay( int )
char check_delay( void )

How to read a button. There is a library routine, but it debounces the buttons, so we're going around it. Buttons and switches are wired to the HC11 digital input ports via some pins on the auxiliary board. That means that a switch state shows up as a bit in the HC11's memory space. Here are some magic locations to access devices. (The names are changed to not collide with some closely related definitions in the header file leap.h.)

#define    DD1        ( PORTE & 0x40 )    // Digital input 1
#define    DIPP1      ( PORTE & 0x10 )    // DIP switch 1
#define    BUTT1      ( PORTG & 0x1 )     // Button1
#define    BUTT2      ( PORTG & 0x4 )     // Button2

Now you can put a statement in your code to determine which device you are testing, for example:

#define    BUTTON    DD1

Using any of the four definitions, the expression BUTTON will return 1 if the button or switch is open, 0 if it is closed. The code above gives you a glimpse of how HC11 IO works. Input and output signals are grouped into "ports". Port E, for example, has 8 digital input signals, which are accessed by an ordinary memory reference at the location defined as PORTE.

How can you tell which device is which? You need a labelled drawing of the Interface Board. Here is a GIF file:

 

When looking at the Interface Board, orient it so that the LEDs look like a traffic light -- red is on top. If you want to work with DIP1, BUT1, or BUT2, just set the DEFINE accordingly and use the chart to find the device. If you want to work with a Lego switch, plug it into J11 and set the DEFINE for D1. Other switches can be connected to J11 using EZ-hooks.

When you first run your code, you should probably set it to one of the buttons. First, see if it gives the right answer, zero, when you don't push the button at all. Next see what happens if you push the buttons a few times. The buttons are not very bouncy, so it should usually produce a correct count of every push and every release of the button.

Once you've gotten the code written and debugged, try it on a variety of devices: a Lego touch sensor switch, the auxiliary board buttons, the auxiliary board DIP switches, a big fat toggle switch. An ideal switch would generate one edge every time you toggle it. An ideal button would generate an edge for each push and an edge for each release. Your experience may vary, but we observed the following:

Perhaps they are all bouncing, but our processor is too slow to see it sometimes.

Why do switches bounce? Think about what really happens when two electrical contacts close. You have two conductors with 5 volts difference, approaching to contact. When they come close enough, there is a small arc, heating and ionizing the atmosphere, carbonizing the contacts, generating a sequence of small nuclear explosions that detonates any nearby NiMH batteries. A computer can check the voltage millions of times per second (well, not your computer, but almost). Each time, it classifies the signal as either a 0 or a 1, depending on how nearly it comes to zero versus five volts. Thus is generated a random sequence of ones and zeros, until the switch settles down.

This raises two important questions: How many times per second can the HC11 check a button? and How long does it take a switch to settle down? While you are thinking about how to debounce a switch you can answer these questions with some more experiments...

Your second assignment is to determine how many times per second the HC11 can check a button. Don't use extreme measures to optimize your code--we're interested in a rough idea of how much an HC11 can do without resorting to extreme measures. You can do this by a simple modification to your earlier code. Add a loop counter to keep track of how many times it looks at the button, and print out the result when you're done.

Perhaps you'll notice something weird. Your counter is probably declared as int, short, or long, all of which are implemented as 16 bit two's complement integers. The highest positive number that can be represented is 32767. If you add 1 to 32767, you get -32768! That is an overflow, and on some computers with some languages it would signal an error. In our case, you just have to anticipate the problem. So, let's say your loop can check a button 50,000 times a second. If you run it for a second, it will print out -15000, more or less. So, how do you know when you are getting a correct answer, without overflow? Try your program with shorter and shorter delays until you are seeing numbers that make sense.

How many times per second can the processor do something? This piece of information is absolutely vital in robot programming, it defines the budget. You have that many "cycles" per second to play with. Our processors are "4 MHz" processors, meaning there are about 4 million microcode cycles per second. An HC11 machine language instruction might take from 2 to 10 microcode cycles or more, so we're down in the range of a million instructions per second. Of course our C loop takes several machine instructions, and you can estimate the number with the result of your experiment. At this point you should ask yourself how many mechanical things your robot can do in a second. Like start and stop a wheel, for example. Not very many! Probably less than ten. Computationally, your robot's brain runs circles around your robot's body. Physically, though, your robot's brain can run circles only by using its body.

Your third assignment is to determine how long it takes for a button to stop bouncing. Let's store a record of the switch readings, and just look at it. Here's some pseudocode:

#define ASIZE 50

int vals[ASIZE];
repeat forever:
	wait for an edge
	vals[0] = button value before edge
	vals[1] = button value after edge
	for i = 2 to ASIZE - 1
		vals[i] = Button
	for i = 0 to ASIZE - 1
		print "." if vals[i] is a 0, "1" if vals[i] is a 1

Now you can really see what's going on---how far apart the bounces can be and so forth. There should be enough information there to decide how to debounce the switch.

So your final assignment is to write a debouncing routine. Forget about the toggle switch, and write a subroutine call that can be substituted for BUTTON in your pulse-counting code, so that no spurious edges are reported.

Here's what we think you learned in session 1:

Session 2: How to drive sort of straight

 

**serious rethinking required. Use of a speed parameter seems to lead inevitably into some very difficult issues. What if we choose a few discrete actions, and always went full speed? Also, maybe this is the time to tell them how motors work? A DC motor, open loop velocity mode. Only reliable speed: zero.**

The motors are commanded via through the motor library routine drive. The prototype is

void drive( motor motor, char percent )

Motor should be either LEFT_MOT or RIGHT_MOT. Percent may vary from -100 to +100, and gives the percentage of full speed, positive for forward and negative for backward.

Let's look at the code for the routine check_bumpers. (This version is edited. The original unedited code is at intro.c.)

void check_bumpers(void)
{
  if(check(RIGHT_BUMP))
    {
      drive(RIGHT_MOT,-100);
      drive(LEFT_MOT,-100);		// Back up
      wait(50);						//  for 50 ticks
      drive(RIGHT_MOT,-20);		// Then back and turn to left
      wait(100);					//  for 100 ticks
    }
  if(check(LEFT_BUMP))
    {
      drive(RIGHT_MOT,-100);
      drive(LEFT_MOT,-100);
      wait(50);
      drive(LEFT_MOT,-20);
      wait(100);
    }
}

(We see a new library routine in use: wait. Earlier you used the routines set_delay and check_delay to keep track of time. Wait is a routine that uses set_delay to set the timer, and then does a busy wait until the timer times out.)

Although the code for check_bumpers works very well, it is a bit mysterious. How far does the robot back up? How sharply does it turn? Through how many degrees does it turn? The motor interface, using the two percentages for the two motors, does not encourage a programmer to think in the most natural way about motion.

In this session you are going to write a set of routines to support a more natural style of robot programming. To go straight ahead approximately 70 cm, the statement could be forward( 70 ). To make a 90 degree right turn with a turning radius of 0 mm (turning in place), the statement could be right( 90, 0 ). Routines backward and left would be similar. Finally we can define routines backleft and backright for those backing turns used in check_bumpers. Using these routines we could rewrite check_bumpers:

void check_bumpers(void)
{
  if(check(RIGHT_BUMP))
    {
		 backward( 100 );
      backleft( 90, 30 );
    }
  if(check(LEFT_BUMP))
    {
      backward( 100 );
      backright( 90, 30 );
    }
}

This is certainly a clearer expression of the programmer's intentions. But you might wonder what happened to the speed parameter. How does the programmer specify speed? The speed parameter seems always to be 100. Having it in every function call clutters the code. Let's just assume it's always 100, and drop that parameter from the function calls. Instead we will have a global variable (shocking!) called speed, which will almost always be 100, but can be adjusted if we ever want to. (If such a use of global variables is too disturbing to you, you may do it differently. For example, you could use defines or an enum so that forward( 50, FULL) and right( 90, HALF, 0) ran at full speed and half speed, respectively.

How do we do it? Let's start with the simplest case to get our feet wet before proceeding to the more difficult cases.

void forward( int distance ){
      drive(RIGHT_MOT,speed);
      drive(LEFT_MOT,speed);
      wait( ??? );
      }

Now, how long does it wait? We know that distance = velocity * time, so we should wait a time equal to distance / velocity. Distance is expressed in centimeters, and time is measured in ticks so we should measure velocity in centimeters per tick. Oh boy, it's time for an experiment! Get your robot going full speed in a straight line. (You could write a quick program, or perhaps it would be easier to unplug the robot's eyeballs and run intro?) Now, let it run for 10 seconds, and record the distance in centimeters. Since there are 10*112.5 ticks in ten seconds, divide the number of centimeters by 1125. That gives you the maximum velocity in centimeters/tick.

(Ack! We don't have any metric measuring instruments in the lab. How absurd. Recall that an inch is 25.4 mm (exactly!), and you can go from there.)

(Don't knock yourself out getting a precise value, because the robots are not going to be very accurate anyway. Differences in the carpet, differences in voltage as the batteries are discharged, lint in the gears, and the phase of the moon all conspire to prevent accurate motions.)

Armed with this key piece of data, we can easily calculate the time delay for a given distance. Unfortunately, the obvious approach turns out not to work. The obvious approach would be as follows:

#define   FULLSPEED    .1778      \\ cms per tick
void forward( int distance ){
      drive(RIGHT_MOT,speed);
      drive(LEFT_MOT,speed);
      wait( (int)((distance/FULLSPEED)*(100.0/speed)) );
      }

(I just made up that number .1778. Your mileage may vary.) Mathematically this is right, and on most computers it would be fine. But you can read the HC11 manual cover to cover, and you won't find any floating point instructions. Our compiler might actually do the right thing, by emulating the floating point in software, but it would be too slow. At least for now, no floating point!

But what is the big deal? The input to our function (distance) is an integer, and we are calculating an integer value (time), so we should be able to recast the computation using just integer arithmetic. It's easier to see what is going on if we do some math with FULLSPEED's value substituted.

( distance / .1778 ) * ( 100 / speed )     // no good
= ( distance * 100 ) / ( .1778 * speed )   // no good
= ( distance * ( 100 / .1778 ) / speed )   // no good, but...
= ( distance * 562 / speed )

We have transformed our calculation into a form that seems to give the right results using integer arithmetic. It involves an approximation but then the parameter FULLSPEED is just a noisy approximation anyway.

So this is your first assignment: Write definitions of forward and backward, using integer arithmetic only. Test them on the robot. You should also define a routine called "stop" that stops the motors. Since any call of forward or backward will just leave the robot rolling when it returns, you need stop.

Besides the lack of floating point, there is another important limitation in the HC11's arithmetic instructions, which you probably ran across in the previous session. Try your implementation of forward with a larger distances -- two meters for example. What goes wrong? You might put some print statements in your code, printing out all the results of your time calculations.

The problem is overflow. The HC11 is an 8-bit microprocessor, with just a few 16-bit arithmetic instructions. Consequently, our compiler implements all three integral types -- short, int, and long -- as 16-bit integers. There is no 32-bit arithmetic. The largest positive signed 16-bit integer is 2^15-1 = 32K - 1 = 32767. Even if we use unsigned integers, the largest is 2^16 - 1 = 64K - 1. So you can get in trouble if distance * 562 >= 32K. You have some choices here:

Perhaps at this point you are disgusted with the limitations of the HC11. If so, consider the following. The HC11 has a user community whose enthusiasm seems unbounded. There is a very active internet newsgroup devoted to the HC11, and the robotics newsgroups are also substantially devoted to HC11 programming and interfacing. It is the first choice of robotics hobbyists, robotics courses, and mechatronics courses everywhere. So hang in there, and by the end of the course perhaps you will be an HC11 fanatic as well. One positive aspect of the HC11 you have already seen: it has good development environments, so that you can program it in C, cross compile, download, and debug. You have also seen how easy it is to interface to switches. Later you will see that it has equally good facilities for interfacing motors and other devices.

Here's what we think you learned in session 2:

Session 3: Going in circles

**Keep the planar kinematics. Ditch the hairy coding issues. Pivoting on a wheel is probably best. Maybe PIVOT left and right, SHARP left and right, SHALLOW left and right **

In the previous session you programmed the robot to go in straight lines (sort of) by driving the motors at the same rates. In this session you will learn to program turning motions by driving the motors at different rates.

Turning is easy, once you understand this simple fact of planar kinematics: every turning motion has a rotation axis. Consider an ordinary automobile making a sharp right turn. Let's say that it has a turning radius of 15 feet. If we extend an imaginary line along the rear axle, and mark a point a distance of 15 feet to the right, we have constructed the rotation axis. The car's motion is exactly the same as if it were attached to a pivot at the rotation axis. Every point in the car is going in a circle centered at the rotation axis.

Things are more complicated when you start moving the steering wheel. When you change the steering wheel, you change the turning radius, and consequently you change the rotation axis.

Our robots work a little differently than a normal car, but the idea of the rotation axis still applies, and it will tell us how to drive the motors. We use the following definitions

R
turning radius in centimeters. Positive is to the right.
r
robot radius in centimeters. (Wheelbase / 2).
v_L
velocity of left wheel, centimeters / second. (Not the rate at which the wheel turns, but the velocity of the center point of the wheel.)
v_R
velocity of right wheel, centimeters / second.
omega
turning rate, radians / second.
 

Given the turning rate and the radii, we could calculate the wheel velocities:

v_L = omega * (R + r)
v_R = omega * (R - r)

Actually, though, we are not given the turning rate omega. We have to calculate the fastest omega so that neither v_L or v_R exceeds the desired speed. If the rotation axis is to the right, so that R is positive, then the left wheel has to travel the fastest. So we set v_L:

v_L = FULLSPEED * speed

Then we solve for omega:

omega = v_L / (R + r)

Now we substitute for omega in the equation for v_R:

v_R = v_L * (R - r) / (R + r)

The case for negative R can be solved in a similar way. Here is some pseudo code:

void turn( speed, radius )
  if R positive
     drive( LEFT_MOT, speed )
     drive( RIGHT_MOT, 
            speed * ( radius - ROBOTRADIUS ) 
            / ( radius + ROBOTRADIUS ) )
   else
     drive( RIGHT_MOT, -speed )
		drive( LEFT_MOT, 
            -speed * ( radius + ROBOTRADIUS ) 
            / ( radius - ROBOTRADIUS ) )

Isn't it cute how FULLSPEED fell out of the picture? Of course you will need to measure and define ROBOTRADIUS. Don't forget what you learned about floating point and integer overflow. For what values of speed and radius will this program work correctly? Your assignment is to turn this pseudo code into real code. Consider carefully whether you want to check for errors in the arguments. Then you need to implement the four routines LEFT, RIGHT, BACKLEFT, and BACKRIGHT. Those four routines should take a desired angle to turn, in degrees, and a turning radius, in centimeters. Both arguments should be positive. The four routines can be implemented by calling your TURN function, followed by a WAIT of appropriate duration. To get you started, here is pseudocode for BACKLEFT:

void backleft( int angle, int radius )
  turn( -speed, radius )
	wait( ( angle * ( radius + ROBOTRADIUS ) )
	       / speed 
	       * DONTASK )

You can see that I had some difficulty calculating the delay while avoiding floating point and avoiding overflow. My solution is pretty ugly, and its resolution suffers -- DONTASK has a value of 18, so my delays have to be multiples of 18. Whatever angle you ask for is going to get rounded down to an angle whose delay is a multiple of 18. It's really worse than that though, because of the division operation. For example, if

angle * ( radius + ROBOTRADIUS ) < speed

then the delay will be zero. ROBOTRADIUS is 5, so a turn in place of less than 20 degrees with a turning radius of zero will produce no motion at all! Someday I'll get back to improving this. If you see a good way to do it please let me know.

What we think you learned in session 3:

Session 4: How to do several things at once

The HC11 is a single-processor system, so at any given time it is only running one program. In that respect it is like most computers. How, then, do computers seem to be doing several things at once? For example, your computer can copy a file, redraw the screen, and listen for keyboard input, seemingly all at the same time. In truth it is only working on one thing at a time, but it switches from one job to the next so quickly that they all seem to be going at the same time.

This idea goes under the name of multi-processing or multi-tasking. The key to the whole idea is a special program called the process scheduler that switches from one process to another. The process scheduler runs whenever there is a clock interrupt -- 112.5 times per second on our system. It keeps a list of active processes, and switches from one to the next in a round-robin fashion.

For the robot, consider a simple example. Suppose we have a program called square:

void square ( void ){  
  int i;
  while( 1 ){
	  for ( i = 4; i--; ){
		  forward( 10 );
		  stop();
		  sleep( 20 );
		  right( 90, 0 );
		  stop();
		  sleep( 20 );
		  }
    }
  }

Notice that we are using the routine sleep () above. It is like wait(), but for multi-tasking. In fact, you probably shouldn't use wait() during multi-tasking, so go back to your motion routines and substitute sleep for wait throughout. One more thing -- sleep( 0 ) does the wrong thing, so add code to make sure that never happens.

The code above would make nice little squares, but it cannot react to the bumpers. For that we might write a second program check_bumpers:

int bumper_event = 0;  
void check_bumpers ( void ){
  while( 1 ){
		if ( check( LEFT_BUMP ) || check( RIGHT_BUMP ) )
			bumper_event = 1;
     next_task();
     }
	}

This checks the bumpers and sets a flag called bumper_event if they are on. Notice that the last statement is a call to next_task. This is a way of telling the process scheduler that it can give the rest of this process' time to the next process.

To make square and check_bumpers run at the same time we have to tell the process scheduler about them. For that we might have the following code:

	int square_id, bumper_id;
	bumper_id = make_task( check_bumpers );
	square_id = make_task( square );
  start_tasking();

Following the call to start_tasking(), the HC11 will let the two tasks check_bumpers and square take turns. At the rate of 112.5 Hertz, the same rate as our delay counter, the process scheduler will switch from one task to the next. So it will spend 80 milliseconds concentrating on squares, then 80 milliseconds concentrating on bumpers, and so forth.

Your assignment is to add one or more additional tasks, so that the robot makes some appropriate response when the bumpers are pushed. Here is some suggested pseudo code, and if you want to crib from my solution, it is at s3dotc.html

void square ( void ){ ... }
int bumper_event = 0;
void check_bumpers ( void ){ ... }

int pointed_left = 0;                // remembers which way
int pointed_right = 0;               //     the robot pointed
void point( void ){
  clear flags pointed_left and pointed_right
  if left_bumper pressed{
    backleft( ... )
    pointed_left = 1
    }
  else if right_bumper pressed{
    backright(... )
    pointed_right = 1
    }
  }

void unpoint( void ){
  if ( pointed_left ) right( ... )
  else if ( pointed_right ) left( ... )
  }

void shake( void ){
  repeat ten times{
    left( ... )
    turn LEDs on
    sleep 10
    right( ... )
    turn LEDs off
    sleep 10
    }
  }

void exec ( void ){
  int square_id, bumper_id;
  bumper_id = make_task( check_bumpers );
  while ( 1 ){
    square_id = make_task( square );
    while( !bumper_event );
      kill_task( square_id );
      point();
      shake();
      stop();
      sleep( 336 );
      bumper_event = 0;
      unpoint();
      }
    }
  }

void main ( void ){
  leap_init();
  make_task( exec );
  start_tasking();

Multi-tasking requires some extra care on the part of the programmer.

 

 

Back to Top of Course Notes