Extending the 3pi RC example

Hello,

Can I first say what a great forum this is. You guys really put a lot of effort in answering questions and I have learnt a lot so far.

I have implemented the rc example in your resources for the 3pi and it works great. I was looking at the suggested improvements and was planning on using an extra channel to do something (not sure what yet). I have 8 channels. It says that it will be best to implement on a separate port. Why is this? As far as I can see there are no pins avaliable on port b which do not involve removing the lcd and this is so valuable for debug i really don’t want to remove it.

On a different subject, I am planning to implement wireless control. I bought an XBEE thinking it was bluetooth. Whoops. It turns out that it is only 2.4 ghz and I need another XBEE to communicate with this. As a general question , where would you start if you would want to implement wireless on 3pi? what would you buy? If you were just starting out, what would you read? Where would you start? I am pretty keen on bluetooth as it would allow me to write a mobile app for a smart phone to control it.

Sorry for this email, it is going on a bit. Just back from the work christmas meal and have had a few. Prob should not be on this forum right now. Don’t answer unless you have nothing and I mean absolutely nothing more important to do. If you do answer then I will be forever grateful.

Anyways… thanks for all.

Tim

Hi tim, thanks for the compliments.

The reason the 3pi RC example says that it would be easy to add a third channel on a port B pin is because you could devote an entirely separate interrupt to processing that channel. Pin-change interrupts on the ATmega328 are grouped based on their ports; all the pins on port B will trigger the same pin-change interrupt when the state of any one of them changes (assuming the PCI has been enabled for all of them). This means that you need to spend time in the ISR determining which pin has changed before you can act, which complicates things. The 3pi RC example is fairly simple; adding another channel on port C or D will make it more complicated.

Note that now the Pololu AVR library has an OrangutanPulseIn library that makes it simple to read RC channels (it handles all of the pin-change interrupt code for you). This portion of the library isn’t yet documented in the user’s guide or command reference, but I wrote a forum thread about it that might help you learn how to use it:

I will be documenting it soon, and, if I can find some time, I’d like to create a version of the Pololu 3pi RC example that uses it.

Note, however, that there really aren’t a lot of available I/O lines on the 3pi, so you won’t easily be able to connect more RC channels without removing the LCD. I recommend against using PD1 because the red LED on that line acts as a strong pull-down that can make the RC signals unreliable or even undetectable (depending on how strongly your RC receiver can drive its outputs). You can try it and see if you get pulses with a high enough voltage to detect, but if you have trouble with signals on PD1, you’ll know why.

I have no experience with wireless control of the 3pi, but it should be pretty easy with a wireless module that outputs TTL (5V) serial (see our serial slave sample program for the 3pi). One of our LV Bots members (Byon) made a wirelessly controllable 3pi, so you might be able to email him and get some pointers (or email him and ask him to reply to this thread, which would benefit everyone who wants to add wireless to the 3pi). You could also try posting to the LV Bots yahoo group.

Good luck with your projects, and please share them with us as they progress!

- Ben

cool thanks. I will give this a go I think.

G’day Ben,
as a 3 week newbie into the world of programmable micro processors, I’ve got myself a 3Pi and have rebuilt a top to house the RC unit.


I’ve also got a wireless cam that might make things a tad more interesting as well.

One thing with the RC receiver is to use the full length of the aerial and dont fold it up. You get full distance so I wound it through a series of 2 pin headers, making sure not to cross it over, soldered in around the top edge of the top board. Full range can exceed several hundred meters as the crow flies, but will give you great but shorter distance in buildings. The camera would make it fun to check on the kids and make sure they’re behaving themselves.

I’m learning very slowly how to program but having fun in the mean time as well.

Great forum and dont you just love learning curves?

clear skies
Stephen

Hi, Stephen.

Thanks for sharing your setup with us; great photos and presentation! How does it work so far? Have you had it up and running? Do you have any videos you could share (maybe something involving the onboard camera)? It looks like it might be a little bit back-heavy with the RC receiver hanging off the back. Have you had any issues with balance?

- Ben

G’day Ben,
heh heh believe it or not, I got caught with the standard xmas glitch… I need to go shopping and get some batteries for the RC transmitter :slight_smile:
I’ve had it running in line follow mode to see how the weight on the tail affects it. So far it looks good. The top of the transmitter box comes off to fit the cables directly to the pins so almost half the weight is removed before running…

I only placed it on the back to make room for the camera, but looking at how little circuitry is involved with the RC receiver, I’m wondering about transfering the actual components to the top board directly. The camera then could sit directly over the top of the board and be low enough to keep the CG down low and all up well balanced.

clear skies
Stephen

G’day Ben,
have replaced the reciever with a much smaller one as per…

everything works like a charm and she’s very quick. I’ve done a vid and will post it as soon as I edit it

clear skies and a happy new year
Stephen

That’s great, I can’t wait to see the video! Did you end up using the OrangutanPulseIn library code, or did you just do it the way the 3piRC example does?

- Ben

G’day Ben,
Happy new year mate… :stuck_out_tongue:
I did it exactlly as described in the 3PiRC instructions. I’m not game enough to start changing things yet heh heh. A bit more reading first.
The vid is 14 meg and I’ll upload it to Utube and then just add a link in my reply to it. I’ve got an uploader but just need to do it.

clear skies
Stephen

G’day Ben,
well that was easy. I now have a youtube acct. The whole 14 meg vid is here…

My son was driving and I was doing the videoing.

clear skies
Stephen

Thanks for sharing the video with us! I look forward to seeing how you customize your RC 3pi, and I hope you share some video from your micro camera. Happy new year to you, too.

- Ben

Hi All,

Yours looks neater than mine!! Nice job.

I have posted a video here too. Soundless I am afraid.

I extended the program a bit to all calibration. Ben, is there a way to store the calibration results in the permanent memory? I run the calibration settings each time at the moment. I don’t think so.

Tim.

Hi Tim,

Thanks for sharing your video! You can store your calibration results in EEPROM, which allows them to persist even if you turn off your 3pi. Note that programming the 3pi using AVR Studio automatically clears all the EEPROM by default, so if you want your calibration values to persist through a reprogramming you need to disable the automatic chip erase that occurs when you program, or you can use AVR Studio to save your EEPROM to a hex file and reprogram the EEPROM with that hex file after you reprogram the flash. Do you know how to read and write to EEPROM?

- Ben

G’day Tim,
sweet job mate. The way mine is, it’s very sensitive to the controls. Yours looks very smooth.

Hey Ben, is there a way you can load more than one program into the 3Pi, say to an mini SD card reader mounted on board, then scroll through the different programs and pick which one you want to run? I assume you could but the amount of onboard memory might restrict it.

I’ve bought another top board, a red one because they go faster :slight_smile: , and will set it up with the RC and camera built in. I’ve also ordered another 3 Pi, addictive little buggars aren’t they?

watch this space :stuck_out_tongue:

clear skies
Stephen

g’day guys,
here’s a couple of mods I’ve come up with…


clear skies
Stephen

Stephen,

The ATmega328 can program itself using a bootloader, so you could potentially write a fancy bootloader that lets you select hex files from an SD card and program your 3pi with them, but that might be more work than it’s worth. I think it would be easier to just make your separate “programs” be separate functions of a single program instead. You could scroll through the available functions and execute the one you choose. Does this make sense?

Thank you for sharing your modifications with us, and please continue to show us what you come up with!

- Ben

G’day Ben,
yep, makes perfect sense.
I need to do a lot more reading and learning before I start doing stuff like that.
The mechanical side is easy for me, it’s the programming that I’ve got to get into now.
Back to the books… :smiley:
thanks for the help mate.

clear skies
Stephen

Hi Ben,

RE EEPROM: Not yet:) I am sure I can figure it out. If there is a sample or web page you can point me at that would be helpful. Thank you.

Stephen - mine is pretty sensitive too. I did customise the mixing code a bit because I was not getting the sample code to max out the motor speed. But did not scale down channel 1 input so it still turns pretty quick. Having said this, it is limited a bit based on forward speed (so the faster forwards you are going the less responsive the turn is). This is the affect that I wanted, but it seems to be a side effect of the code really.

If you are interested I will post the code - I might update it with the EEPROM change first though.

Tim.

Edit: I have had a quick look at read/write to eeprom using the funtions defined in <avr/eeprom.h>. I should be ok with this. I will let you know if not.

Well, here it is. This is my updated code which allows calibration and stores the results in the eeprom program space. Any comments and suggestions welcome.

Tim.


/*
 
 * RC 3pi
 *
 * This 3pi robot program reads two standard radio-control (RC) channels and mixes 
 * them into motor control. Channel zero (connected to the PD0 input) 
 * handles forward and reverse, and channel one (connected to the 
 * PC5 input) handles turning.
 *

I am using an 8 channel input, connect to channels 1 and 3 on the rc receiver
and to the pins on the 3pi in the positions shown below.

 1          2       3    4  5     6     7
------------------------------------------
|CH3 NEG|CH3 SIG|       |  |  |CH1 SIG|  |  
------------------------------------------
|       |       |CH1 POS|  |  |       |  |
------------------------------------------

CH3 SIG = PD0
CH1 SIG = PC5

 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <pololu/3pi.h>
#include <avr/eeprom.h>

#define bool short
#define true 1
#define false 0

// define the full battery charge and empty battery charge so that
// it will display % charged to the user on button c press.
#define FULL_BAT_MV 5800
#define EMPTY_BAT_MV 4900

/////
// Standard startup function. I always display the battery
// voltage and percentage charged at the start of the programs
// as letting it go flat and re-programming it seems a very bad idea.
/////
void waitForStartCommand() ; 

// I want to store the min/mid/max values for channels 1 and 2 in the eeprom
// memory so that I do not have to run the calibration each time. Define the
// initial values here as well. 
// **** Remember to program these onto the robot too from the .eeprom file. ***.
uint16_t EEMEM ch0_min = 300;
uint16_t EEMEM ch0_mid = 450;
uint16_t EEMEM ch0_max = 600;

uint16_t EEMEM ch1_min = 300;
uint16_t EEMEM ch1_mid = 450;
uint16_t EEMEM ch1_max = 600;

// This is to catch errors. 
const int maxLowPulseTime  = 3000; 

struct ChannelStruct
{
	// volatile as they are going to change within the isr.
	volatile unsigned int prevTime;
	volatile unsigned int lowDur;
	volatile unsigned int highDur;
	volatile unsigned char newPulse;

	unsigned int pulse;
	unsigned char error;

	// Each channel now has its own set of min/max/mid stick positions. 	
	int min ;
	int mid ;
	int max ;
} ; 


// capture the two channels.
struct ChannelStruct ch[2];

/*
 * Pin Change interrupts
 * PCI0 triggers on PCINT7..0
 * PCI1 triggers on PCINT14..8
 * PCI2 triggers on PCINT23..16
 * PCMSK2, PCMSK1, PCMSK0 registers control which pins contribute.
 *
 * The following table is useful:
 *
 * AVR pin    PCINT #            PCI #
 * ---------  -----------------  -----
 * PB0 - PB5  PCINT0 - PCINT5    PCI0
 * PC0 - PC5  PCINT8 - PCINT13   PCI1
 * PD0 - PD7  PCINT16 - PCINT23  PCI2
 *
 */

// This interrupt service routine is for the channel connected to PD0
ISR(PCINT2_vect)
{
	// Save a snapshot of PIND at the current time
	unsigned char pind = PIND;
	unsigned int time = TCNT1;

	if (pind & (1 << PORTD0)) 
	{
		// PD0 has changed to high so record the low pulse's duration
		ch[0].lowDur = time - ch[0].prevTime;
	}
	else
	{
		// PD0 has changed to low so record the high pulse's duration
		ch[0].highDur = time - ch[0].prevTime;
		ch[0].newPulse = 1; // The high pulse just finished so we can process it now
	}
	ch[0].prevTime = time;
}

// This interrupt service routine is for the channel connected to PC5
ISR(PCINT1_vect)
{
	// Save a snapshot of PINC at the current time
	unsigned char pinc = PINC;
	unsigned int time = TCNT1;

	if (pinc & (1 << PORTC5))
	{
		// PC5 has changed to high so record the low pulse's duration
		ch[1].lowDur = time - ch[1].prevTime;
	}
	else
	{
		// PC5 has changed to low so record the high pulse's duration
		ch[1].highDur = time - ch[1].prevTime;
		ch[1].newPulse = 1; // The high pulse just finished so we can process it now
	}
	ch[1].prevTime = time;
}


/**
 * updateChannels ensures the recevied signals are valid, and if they are valid 
 * it stores the most recent high pulse for each channel.
 */ 
void updateChannels(bool calibrating)
{
	unsigned char i;

	for (i = 0; i < 2; i++)
	{
		cli(); // Disable interrupts
		if (TCNT1 - ch[i].prevTime > 35000)
		{
			// The pulse is too long (longer than 112 ms); register an error 
			// before it causes possible problems.
			ch[i].error = 5; // wait for 5 good pulses before trusting the signal

		}
		sei(); // Enable interrupts

		if (ch[i].newPulse)
		{
			cli(); // Disable interrupts while reading highDur and lowDur
			ch[i].newPulse = 0;
			unsigned int highDuration = ch[i].highDur;
			unsigned int lowDuration = ch[i].lowDur;
			sei(); // Enable interrupts

			ch[i].pulse = 0;

			// Humm. I seem to get a bit of error at maximum speed/turn. This seems
			// to be because it is 1 or 2 over the min and max positions. Add a little
			// margin for error here before calling the pulse an error. 
			int maxPulse = ch[i].max + 50 ; 
			int minPulse = ch[i].min - 50 ;  

			// Note that we only test for out of range values on the min and max
			// when we are not calibrating, otherwise we don't know what are good
			// values.
			if (lowDuration < maxLowPulseTime || 
					( !calibrating && (highDuration < minPulse || 
					highDuration > maxPulse) ) )
			{
				// The low pulse was too short or the high pulse was too long or too short
				ch[i].error = 5; // Wait for 5 good pulses before trusting the signal
			}
			else
			{
				// Wait for error number of good pulses
				if (ch[i].error)
					ch[i].error--;
				else
				{
					// Save the duration of the high pulse for use in the channel mixing
					// calculation below
					ch[i].pulse = highDuration; 
				}
			}
		}
	}
}

void printCalibrationResults( long ch0, long ch1 )
{
	clear() ; 
	print( "Ch0" ) ; 
	lcd_goto_xy(0,1) ; 
	print_long( ch0 );
	delay_ms ( 500 ) ; 
	
	clear() ; 
	print( "Ch1" ) ; 
	lcd_goto_xy( 0,1 ) ; 
	print_long( ch1 ) ; 
	delay_ms(500) ;
	  
}

void getCalibration( const char* text, int *ch0, int *ch1 )
{
	clear() ; 
	print( text ) ;
	lcd_goto_xy( 0,1 );  
	print( "Then B" ) ; 
	// wait for a button b press and a good signal. the user should have moved
	// the position of the sticks during this time.
	while(!button_is_pressed(BUTTON_B) ||
		ch[0].error || ch[1].error )
	{
		updateChannels(true) ; 
	}
	// capture this position.
	*ch0 = ch[0].pulse ;
	*ch1 = ch[1].pulse ;
}

void calibrate()
{
	// Get the min/max and middle sticks and store on the channels.
	getCalibration( "Middle", &ch[0].mid, &ch[1].mid) ; 
	printCalibrationResults( ch[0].mid, ch[1].mid ) ; 
	
	getCalibration( "Max/Rght", &ch[0].max, &ch[1].max) ; 
	printCalibrationResults( ch[0].max, ch[1].max ) ; 
	
	getCalibration( "Min/Left", &ch[0].min, &ch[1].min) ; 
	printCalibrationResults( ch[0].min, ch[1].min ) ;
}

void loadSettingsFromEEPROM()
{
	// for both channels load the settings from eebrom. Note that
	// we use the eeprom functions for this and pass the address
	// of the int within the eeprom program space as an argument to
	// this function.
	ch[0].min = eeprom_read_word(&ch0_min); 
	ch[0].mid = eeprom_read_word(&ch0_mid);
	ch[0].max = eeprom_read_word(&ch0_max);

	ch[1].min = eeprom_read_word(&ch1_min);
	ch[1].mid = eeprom_read_word(&ch1_mid);
	ch[1].max = eeprom_read_word(&ch1_max);

	// debug. print the loaded settings.
	printCalibrationResults( ch[0].min, ch[1].min ) ;
	printCalibrationResults( ch[0].mid, ch[1].mid ) ;
	printCalibrationResults( ch[0].max, ch[1].max ) ;
	
	// TODO: Catch errors. Check that the .eeprom file has been programmed
	// onto the robot.  
}

void writeSettingsToEEPROM()
{
	// Save the current settings so that they persist when the robot is
	// turned off.
	eeprom_write_word(&ch0_min, ch[0].min) ;
	eeprom_write_word(&ch0_mid, ch[0].mid) ; 
	eeprom_write_word(&ch0_max, ch[0].max) ;
	
	eeprom_write_word(&ch1_min, ch[1].min) ;
	eeprom_write_word(&ch1_mid, ch[1].mid) ; 
	eeprom_write_word(&ch1_max, ch[1].max) ;   
}

unsigned int errortime = 0 ; 

void drive()
{
	updateChannels(false);

	if (ch[0].error || ch[1].error)
	{
		// This bit of code is a bit experimental really. I am attempting to
		// give a delay when the signal is lost of 500ms to avoid small gitches.
		// the user won't notice this loss of control. 
		// TODO: Test this code - not sure it works correctly.
		unsigned int time = TCNT1 ;
		if ( errortime == 0 )
		{
			errortime = time + 500 ; 
			return ; 
		} 
		else if ( time < errortime )
		{
			// don't shut down yet
			return ;
		} 
		// Ok switch off.
		errortime = 0 ; 
		set_motors(0, 0);
		return  ; 
	}

	// good result so reset error time
	errortime = 0 ; 
	
	// This is basically the mix code from the example, but i scale each channel between
	// -255 and 255 first and then mix them. When forwards = 0,0, it will allow -255,255 of rotation.
	// when forwards is 255,255 it will give 0,255 of rotation.
	// TODO: Make this neater.
	long forwards = -(ch[0].mid - (int)ch[0].pulse) ;  
	long rotation = ((int)ch[1].pulse) - ch[1].mid ; 
	
	// scale both forwards and rotation so that they are -255 to 255.
	forwards = forwards * 255 / ( ch[0].mid - ch[0].min ) ; 
	rotation = rotation * 255 / ( ch[1].mid - ch[1].min ) ; 

	// now we simply do the mix. If we are not going forwards we can rotate
	// faster (i.e. +255,-255) than it we are going flat out as this would result
	// in 255,0. This is fine and what we want.
	long m1 = forwards + rotation ; 	          
	long m2 = forwards - rotation ;  

	// Clamp the values.
	if ( m1 > 255 )
		m1 = 255 ; 
	if ( m1 < -255 )
		m1 = -255 ; 

	if ( m2 > 255 )
		m2 = 255 ; 
	if ( m2 < -255 )
		m2 = -255 ; 

	// Print the motors speeds.
	if (get_ms() % 1000) 
	{
		lcd_goto_xy(0, 0);
		print("m1 ");
		print_long(m1);  
		print("    ");
		lcd_goto_xy(0, 1);
		print("m2 ");
		print_long(m2);
		print("    ");
	}			

	set_motors(m1, m2);
}

int main()
{
	// wait until b is pressed before starting.
	waitForStartCommand() ; 

	ch[0].error = 5; // Wait for 5 good pulses before trusting the signal
	ch[1].error = 5; 

	DDRD &= ~(1 << PORTD0);	// Set pin PD0 as an input
	PORTD |= 1 << PORTD0;	// Enable pull-up on pin PD0 so that it isn't floating
	DDRC &= ~(1 << PORTC5); // Set pin PC5 as an input
	PORTC |= 1 << PORTC5;	// Enable pull-up on pin PC5 so that it isn't floating
	delay_ms(1);			// Give the pull-up voltage time to rise
	
	PCMSK1 = (1 << PORTC5);	// Set pin-change interrupt mask for pin PC5
	PCMSK2 = (1 << PORTD0);	// Set pin-change interrupt mask for pin PD0
	PCIFR = 0xFF;			// Clear all pin-change interrupt flags
	PCICR = 0x06;			// Enable pin-change interrupt for masked pins of PORTD
							//  and PORTC; disable pin-change interrupts for PORTB
	sei();					// Interrupts are off by default so enable them

	TCCR1B = 0x03;	// Timer 1 ticks at 20MHz/64 = 312.5kHz (1 tick per 3.2us)

	// load the calibration settings from eeprom.
	loadSettingsFromEEPROM() ; 

	clear() ; 
	print( "B>Start" );
	lcd_goto_xy(0,1) ; 
	print( "C>Setup" ) ; 
	
	// Ok, wait for another button B press to start. Button C
	// launches the calibration. 
	while(1)
	{	
		if ( button_is_pressed(BUTTON_B) )
			break ; 
		if ( button_is_pressed(BUTTON_C) )
		{
			calibrate() ;
			
			// Save the settings so that they persist when turned off.
			writeSettingsToEEPROM() ; 
			
			clear() ;
			print( "B>Start" );
			lcd_goto_xy(0,1) ; 
			print( "C>Setup" ) ; 
		}
	}  

	// Now just do the drive code forever.
	while (1) 
	{
		drive() ; 
	}

    // This part of the code is never reached.  A robot should
    // never reach the end of its program, or unpredictable behavior
    // will result as random code starts getting executed.  
}


/////
// Startup functions.
/////
void displayBattVoltage()
{
	int bat = read_battery_millivolts();
	clear();
	print_long(bat);
	print("mV");
}

void displayBattPercent()
{
	int bat = read_battery_millivolts() - EMPTY_BAT_MV;
	float p = ((float)bat / (FULL_BAT_MV-EMPTY_BAT_MV))*100.0 ; 
	clear();
	print_long((int)p);
	print("%");
}

// waits for a button b press before running the main application.
void waitForStartCommand()
{
	typedef void (*DISPLAY)(void);  

	DISPLAY cb = displayBattVoltage ; 

	// I always start a program with button B and display
	// the voltage or percentage.
	while(!button_is_pressed(BUTTON_B))
	{
		cb() ; 
		lcd_goto_xy(0,1);
		print( "Press B" );
		if ( button_is_pressed(BUTTON_C) )
		{
			cb = displayBattPercent; 
		}
		else if ( button_is_pressed(BUTTON_A) )
		{
			cb = displayBattVoltage ; 
		}
		delay_ms(100);
	}

	clear() ; 
	// delay a bit after the press before doing stuff. 
	delay_ms(1000);
}

Hello,

I think the reason our RC 3pi looks so smooth is because I had driven it around the office a lot before taking the video, and then I was definitely not driving it at full speed during the video. Also, I cut the video off at a point where I had just driven the 3pi into a vacuum cleaner that was off frame!

That mod to protect the IR sensors is nice, when I was driving my 3pi off 1’ steps in my house onto tile floors, I ripped the sensor (and its pad!) off. Maybe that LED would have saved it.

- Ryan