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

Interfacing the GP2D02 to a Microchip PIC and Sweeping it with a Hobby Servo
Written by Aaron Ramsey

Overview

This article describes part of the sensor system that I used on a series of robots. The robots were built as part of my degree project in my last semester of university. I was limited by the amount of money that I could spend on each robot (as we likely all are!), and I had to find solutions to give the robots a reasonable view of the world around them. For the main sensor, I used a Sharp GP2D02 IR distance sensor mounted on a servo which swept it back and forth in front of the robot.

Several possibilities were considered for a primary distance measuring device for the front of the robots. Laser, CCD camera, ultrasonic and infrared were all studied. Laser and CCD both proved to be to expensive and difficult to implement. Ultrasonics have several good points, such as range of detection and reasonable easy to implement. I found two problems which I were unable to overcome at the time. The transceivers which I was able to afford had a very wide angle of detection, and it was difficult to pinpoint objects. Also, because sound reflects very easily, there were a large amount of false readings. Both shortcomings can be overcome, but I moved on to IR sensors. I decided to use a Sharp GP2D02 rather than design my own, as the Sharp offered better resolution and more accurate readings than the sensors that I was able to build myself. They are also very easy to use.

Sharp GP2D02

The Sharp GP2D02 is a sensitive compact distance measuring sensor. It required two lines from a microcontroller in order to be controlled. One line provides the signal to begin a measurement and also is used to provide a clock signal when transmitting the distance measure, and the other line is used to transmit the measurements back to the microcontroller. I interfaced the GP2D02 to a 12CE519 microcontroller rather than my main CPU (16C77) in order to free up processing time on the 16C77. The GP2D02 requires an open collector on its input line, so I connected it through a diode to the 12CE519. The GP2D02 output is connected directly to the 12CE519. As I was limited to one GP2D02 IR sensor per robot, I used a hobby servo motor to sweep the GP2D02 through a 50 degree pattern in the front of the robot. The servo used was a Cirrus CS-70 Standard Pro Servo.

The GP2D02 is a self contained device which emits an IR pulse and determines the distance of a nearby object using triangulation. It is able to measure distances up to 80 cm and at that range has a beam width of only 10 cm. I mounted this sensor on servo motor at the front of the robot. The servo sweeps the sensor through a 50 degree pattern. The servo is discussed in more detail later. The sensor is digitally controlled with its Vin line. The Vin line is pulsed low to tell the sensor to begin a measurement. The sensor will output a high on the Vout line when it is ready to transmit. The Vin line is then pulsed, and the sensor data is clocked in on the Vout line. This is illustrated in the figure below.

gp2d02 timing
Figure 1 - GP2D02 Timing

The C code to implement this is fairly easy. I used the CCS C compiler. The code is fairly readable though, and could be easily converted to any language.


int get_ir_reading() {
	int counter=9; // a kludge, see above
	int reading=0;

	output_low(GP2D02_VIN); 	// start cycle with Vin low
	delay_ms(1); 			// give the sensor a little time before we bug it
	while (!input(GP2D02_VOUT)); 	//wait for Vout to go high

	do {
		delay_cycles(4);	// minimum wait is 2us, max is 170us, 4 worked
		output_low(GP2D02_VIN);  // tell the sensor that we want a bit
		delay_cycles(2); 	// might be as low as 1 (maybe), 2 worked

		reading=reading<<1; 	//left shift the reading value
		reading=reading|(input(GP2D02_VOUT)); // put the newest reading into the
						 // LSB of reading
		output_high(GP2D02_VIN); // we read the bit, and get ready for the next one
		counter--;
	} while (counter>0);

	// We leave the Vin pin high after finishing the reading
	// This resets the sensor in order for a new reading next
	// time

	// An 8 bit number indicating the distance should now be
	// sitting in the variable 'reading'

	return(reading);
}


GP2D02 Measurements

The distance measurement is a 8 bit number. It is not linear, as can be seen in Figure 2 below. The distance measurement can be linearized using the following formula proposed by Sean H. Breheny.

Linearized data = 1.9/(tan(reading*25/1000))

The constants in the formula above were established by Mr. Breheny through experimental means. Using my measured data from 3 GP2D02 sensors, I was unable to improve the linearization calculations by manipulating the constants, and the formula was implemented as is. I later discovered that reasonable linearization can be achieved by simply inverting the reading received (1/reading) and then multiplying it by some constant. This eliminates the tan term, which is not a pretty thing in a 8 bit microcontroller to implement. <grin> Actually, I ended up not linearizing the data at all in the robot. I designed the robot around a behavioural model (ie- instincts rather than brains) and it was just as easy to use the real sensor readings. Linearizing the readings would be very useful if you were trying to create a map from what the robot was seeing. I will be looking deeper into this in a future design.

Raw Data Figure 2 - Raw reading from GP2D02
Linear Data Figure 3 - Linearized reading from GP2D02

As can be seen from the graphs above, the sensors have a lower limit. At around 7cm, the value read peaks and then begins to fall again. In other words, an object closer then 7 cm will appear to be further away. If we do not 'see' the object before it enters this zone, this will become a large problem with this particular sensor. This is another good reason for the robots to move slowly. This is also an excellent reason for having bumpers on a robot.

Communicating with the Main CPU

The GP2D02 is not connected directly to the main CPU on the robot. It is controlled by a 12CE519, which also controls the servo. This frees up the main CPU for other tasks and also provided a IR module which can easily be transferred to projects later. This can be a very useful approach. The 8 pin pics such as the 12C509, etc. are extremely cheap and are very powerful. By dividing the tasks up and moving them off the main cpu, you can create a very power design and avoid having to use very expensive processors. Another advantage of this is that the 'modules' that you create are easily transferred to the next design that you create.

When the 12CE519 has taken a reading from the GP2D02, it transmits the readings to the 16C77. The GP2D02_TX line is hooked to RB5 on the main CPU. When it goes high, an interrupt vector is created on the 16C77. The main CPU then reads in the eight bits of data. The communication between the two processors occurs at over 63 kbps and the transmission of data takes around 1 ms of time. The main CPU has quite a bit of idle time on its hands, so I was able to read in the data entirely from within this interrupt on the 16C77. This saves us some fancy programming, and also makes the data transfer as fast as possible. We want to have as many GP2D02 readings as possible as we move, and we don't want the sensor waiting while its controller communicates to the main cpu. More on the timing considerations later.

The communication protocol between the CE519 and C77 is as follows...

1)The 519 puts the TX line high when it is ready to transmit
2)The C77 recognizes the high on the TX line (it is connected to an interrupt line), and puts a high on the clock line to tell the 519 to clock the first bit of data on the TX line
3)The 519 sees the 1st clock transition on the clock line and puts up the first bit of data
4)The C77 waits approx. 20 us (16 us min) and then puts the clock line low to tell the 519 to put the next bit of data on the TX line
5)The 519 sees the transition on the clock line, and puts the next bit of data on the TX line
6)This continues... The C77 waits 20 us, then changes the polarity of the clock line. This transition is what tells the 519 to transmit the next bit
7)We have a fixed packet size of 8 bits, so after 8 bits are TX'd, the 519 goes back to get another reading, and the C77 goes about it's merry way.

Relevant code - 12CE519 (GP2D02 and servo controller)

void transmit_data(int data) {
	// tell the main CPU that we want to send data
	output_high(SERIAL_TX);

	while (!input(SERIAL_CLK)); //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,7));
	while (input(SERIAL_CLK));  //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,6));
	while (!input(SERIAL_CLK)); //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,5));
	while (input(SERIAL_CLK));  //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,4));
	while (!input(SERIAL_CLK)); //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,3));
	while (input(SERIAL_CLK));  //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,2));
	while (!input(SERIAL_CLK)); //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,1));
	while (input(SERIAL_CLK));  //wait for main CPU to signal
	output_bit(SERIAL_TX,bit_test(data,0));

	// make sure that after tx'ing, we leave the data line low
	// so that the main cpu does think that we have more data
	// to come.
	delay_us(10); //make sure the last bit is valid
	output_low(SERIAL_TX);
}

Relevant code - 16C77 (main cpu)

#int_rb
b4567_interrupts() {
	// check if we have data coming in from the GP2D02 controller
	if (input(GP2D02_TX)) {
		// send an 'acknowledge' signal and also the first
		// clock signal
		output_high(GP2D02_CLK);
		for (counter=0;counter<4;counter++) {
			output_high(GP2D02_CLK);
			delay_us(15); // 15 instead of 20
			// This next line takes 24 assembly instructions,
			// which is just under 5 us
			gp_reading[gp_position]=(gp_reading[gp_position]<<1)|((int)input(GP2D02_TX));
			output_low(GP2D02_CLK);
			delay_us(15); // 15 instead of 20
			// This next line takes 24 assembly instructions
			gp_reading[gp_position]=(gp_reading[gp_position]<<1)|((int)input(GP2D02_TX));
		}
		// note that the last clock output is a low, which
		// is needed otherwise next time the 519 goes to TX,
		// it would see a high on the clock line and would start
		// transmitting before the receive_data() procedure was
		// ready to go
	}
}

How the servo fits in

As I was limited to one GP2D02 IR sensor per robot, I used a hobby servo motor to sweep the GP2D02 through a 50 degree pattern in the front of the robot. The servo used was a Cirrus CS-70 Standard Pro Servo. It is rated 0.15 sec/60 degrees at 4.8 volts and 0.12 sec/60 degrees at 6.0 volts. It is powered by 5 volts. A servo is controlled by a series of pulses with certain high and low times. The high time controls the position of the servo. A typical servo accepts a 1 ms to 2 ms high pulse in order to position itself over a 110 degree sweep. The low time can vary from 10 ms to 20 ms, and plays no role in the position of the servo.

Servo Pulse Servo Pulse Width

After some experimentation, it was found that the speed rating of the servo was not the entire truth. This application needed to be accurate and repeatable with a relatively small change in position. We are moving only 8.33 degrees per position change. If we were to trust the speed rating, we would be able to achieve this change in only 20.8 ms. This is not the case though. In order to position it accurately, I actually needed to give it 6 pulses, each around 12 ms long. Originally I had wanted to sweep wider than 50 degrees, but the time to position the servo left me very few choices.

The controlling 12CE519 takes 7 readings from the Sharp GP2D02 in a pattern over 50.12 degrees, sending the readings out as they are collected. It operates a sweep in one direction, when sweeps backwards in the opposite direction to reach the starting position. This saves the wasted time of moving the servo through 50 degrees to the start position without taking readings.

Sample Servo Code

// do a sweep in the CCW direction
for (position=134;position<175;position=position+8) {
	reading=get_ir_reading();
	transmit_data(reading);
	for (counter2=0;counter2<6;counter2++) {
		counter1=0;
		output_high(SERVO_CONTROL);
		do {
			delay_us(4);
			counter1++;
		} while (counter1<position);
		output_low(SERVO_CONTROL);
		delay_ms(10);
	}
}

Timing Considerations

The GP2D02 takes on average 60 ms to take a reading and transmit it to the 12CE519 controller and the servo takes approximately 70 ms to move from one position to the next. The 12CE519 takes only 1 ms to transmit to the 16C77 and will be neglected for these purposes. The GP2D02 takes 7 readings in one sweep. This means that it takes around 840 ms (6*70+7*60) to do one full sweep. It also means that if an object appears on one side immediately after a reading is taken, it will be 1560 ms (12*70+12*60) before that object is detected. The servo will have to sweep to the wrong side and back before the object is in its range. This is worst case of course. The sweep pattern that I set out is much wider than the robot base. Even if we don't detect an object at the edges of the pattern, we will not hit it. The more important centre reading will never be more than 710 ms (5*70+6*60) away from a new reading. This all results in an upper limit on our forward speed. If we travel 80 cm in 710 ms, it is very possible that we might not see an object before hitting it. The robots are geared so that they travel 25 cm in 1 second, so they are well under the maximum speed. This slow speed is not a factor in this design, but if I had wanted faster robots, I would have had to rethink the sensors. Multiple IR sensors on the front of the robot would allow the robot to travel much faster, as would having an early warning system built from an ultrasonic sensor.

I am currently experimenting with homebrew ultrasonics again and I would like to combine the two types of sensors to create a more robust detection system.

Conclusion

Well, I'm surprised that you are still with me at this point. I won't blame you if your eyes glazed over and you skipped over most of this article. I'll summarize the main points to take away from this discussion for you then....

1) GP2D02 sensors are very accurate and cool, but bloody expensive
2) The GP2D02 is very easy to interface to a microprocessor 3) A servo can be used to sweep the GP2D02 sensor back and forth in order to simulate multiple GP2D02s
4) A servo is also very easy to interface to a microprocessor
5) The 8 pin PICs can be used to move tasks away from the main robot CPU in order to free up processing power and ram on the main CPU.
6) Finally, Aaron needs to learn how to write an interesting article!

I would be happy to try and explain any of this to anyone who doesn't see the point. Just give me an email at aaron@bottle-rocket.org.



***********************************************************
The entire C code that was programmed into the 12CE519 PICs
***********************************************************

***********************************************************
The entire C code that was programmed into the 12CE519 PICs
***********************************************************


//------------------------------------------------------------------
// SERVO_GP2D02.C 	Written by Aaron Ramsey, Jan.8th, 2000
//
// This program takes 7 readings from the Sharp GP2D02
// in a pattern over 50.12 degrees, sending the readings
// out as they are collected.
//
// It operates a sweep in one direction, when sweeps
// backwards in the opposite direction to reach the
// starting position. This saves the wasted time of
// moving the servo through 50 degrees without taking
// readings.
//
// It takes approx. 50ms to take a reading, and 60ms to
// move from servo position to servo position.
//
// TO DO - We are planning to possible implement a mode where
// 			if GP5 is made high, the servo makes a 25 position
//			sweep rather than the regular 7 position. This
//			would only be a useful mode when the robot is
//			stopped as a full sweep would take almost 3 seconds.
//
// Currently it is implemented on a pic12ce519 running at
// 4MHz.
//
//	PINS USED
//		GP0 - Servo control line
//		GP1 - Serial transmit
//		GP2 - Serial receive
//		GP3 - GP2D02 Vout line
//		GP4 - GP2D02 Vin line
//		GP5 - Sweep Pattern (low for 7 readings over 50
//				degrees ,high for 25 readings over 180 degrees)
//------------------------------------------------------------------

#include <12ce519.h>
//#include <16f84.h>

#fuses INTRC, NOWDT, NOPROTECT, NOMCLR
//#fuses NOWDT,NOPROTECT,HS

//OSCCAL SETTING! MAKE SURE TO READ IT AND CHANGE THIS TO MATCH
//BEFORE PROGRAMMING ANY CHIPS!
//#rom 0x3FF={0xC70}

#use delay(clock=4000000)

#define SERVO_CONTROL	pin_B0
#define SERIAL_TX		pin_B1
#define SERIAL_CLK		pin_B2
#define GP2D02_VOUT		pin_B3
#define GP2D02_VIN		pin_B4
#define SWEEP_PATTERN	pin_B5	// unimplemented

int get_ir_reading();
void transmit_data(int data);

//------------------------------------------------------------------

void main(void) {
	int counter1,counter2=0;
	int position,num=0;
	int reading;

	set_tris_b(0b01101100);	//all out except GP2,GP3,GP5

	// initialize the servo to a known position... all the
	// way to the right
	position=126;
	for (counter2=0;counter2<50;counter2++) {
		counter1=0;
		output_high(SERVO_CONTROL);
		do {
			delay_us(4);
			counter1++;
		} while (counter1&lt;position);
		output_low(SERVO_CONTROL);
		delay_ms(10);
	}

	// main loop
	do {
		// do a sweep in the CCW direction
		for (position=134;position<175;position=position+8) {
			reading=get_ir_reading();
			transmit_data(reading);
			for (counter2=0;counter2<6;counter2++) {
				counter1=0;
				output_high(SERVO_CONTROL);
				do {
					delay_us(4);
					counter1++;
				} while (counter1&lt;position);
				output_low(SERVO_CONTROL);
				delay_ms(10);
			}
		}

		// do a sweep in the CW direction
		for (position=166;position>125;position=position-8) {
			reading=get_ir_reading();
			transmit_data(reading);
			for (counter2=0;counter2<6;counter2++) {
				counter1=0;
				output_high(SERVO_CONTROL);
				do {
					delay_us(4);
					counter1++;
				} while (counter1&lt;position);
				output_low(SERVO_CONTROL);
				delay_ms(10);
			}
		}

	} while (TRUE);
}

//-------------------------------------------------------
// int get_ir_reading()
//
// Gets a reading from the sharp GP2D02 IR sensor.
// It reads the data from the GP2D02 1 bit at a time
// by left shifting the data into the varible 'reading'.
// I ran into some trouble with this routine. Originally
// I was clocking in 8 bits of information, but the first
// bit was always 1... giving me a range of values from
// ~170 to 240 or so, which pretty much sucked. After some
// testing, I found that by clocking in 9 bits and simply
// throwing away the first one, I could get readings from
// ~80 to 240. At this point, I do not know why this was
// happening. The data sheets are pretty minimal. Anyways,
// it works well now.
//-------------------------------------------------------

int get_ir_reading() {
	int counter=9; // a kludge, see above
	int reading=0;

	output_low(GP2D02_VIN); // start cycle with Vin low
	delay_ms(1); 	// give the sensor a little time before we bug it
	while (!input(GP2D02_VOUT)); //wait for Vout to go high

	do {
		delay_cycles(4);	// minimum wait is 2us, max is 170us, 4 worked
		output_low(GP2D02_VIN);  // tell the sensor that we want a bit
		delay_cycles(2); // might be as low as 1 (maybe), 2 worked

		reading=reading<<1; //left shift the reading value
		reading=reading|(input(GP2D02_VOUT)); // put the newest reading into the
						 // LSB of reading
		output_high(GP2D02_VIN); // we read the bit, and get ready for the next one
		counter--;
	} while (counter>0);

	// We leave the Vin pin high after finishing the reading
	// This resets the sensor in order for a new reading next
	// time

	// An 8 bit number indicating the distance should now be
	// sitting in the variable 'reading'

	return(reading);
}


//-----------------------------------------------------------
// void transmit_data()
//
// This procedure assumes B1 is TX to main cpu, and B2 is
// clock line. The 519 can't sweep and TX at the same time,
// so we send a high to the main cpu to let it know that we
// want to transmit, then we sit and wait until the main
// cpu provides us with the TX clock. That clock does not
// need to be any particular speed... it simply should be
// as fast as possible, so that we can get the 519 back to
// do another GP2D02 sweep. The main cpu provides a clock
// because we don't trust the internal RC oscillator that
// the 519 is running off of, particularily at the 40+kbps
// that we hope to be running this routine at.
//
// The comm protocol is as follows...
//		1) The 519 puts the TX line high when it is ready
//			to transmit
//		2) The c77 recognizes the high on the TX line, and
//			puts a high on the clock line to tell the 519
//			to clock the first bit of data on the TX line
//		3) The 519 sees the 1st clock transistion on the
//			clock line and puts up the first bit of data
//		4) The c77 waits approx. 20us (16us min) and then
//			puts the clock line low to tell the 519 to
//			put the next bit of data on the TX line
//		5) The 519 sees the transistion on the clock line,
//			and puts the next bit of data on the TX line
//		6) This continues... The c77 waits 20us, then changes
//			the polarity of the clock line. This transistion
//			is what tells the 519 to transmit the next bit
//		7) We have a fixed packet size of 8 bits , so
//			after 8 bits are TX'd, the 519 goes back to get
//			another reading, and the c77 goes about it's
//			merry way.
//------------------------------------------------------------
void transmit_data(int data) {
	// tell the main CPU that we want to send data
	output_high(SERIAL_TX);

	while (!input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,7));
	while (input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,6));
	while (!input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,5));
	while (input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,4));
	while (!input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,3));
	while (input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,2));
	while (!input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,1));
	while (input(SERIAL_CLK));
	output_bit(SERIAL_TX,bit_test(data,0));

	// make sure that after tx'ing, we leave the data line low
	// so that the main cpu does think that we have more data
	// to come.
	delay_us(10); //make sure the last bit is valid
	output_low(SERIAL_TX);
}