Adding an I2C Compass to the AVR Atmega32 04/06/2009Posted by aliasmrjones in The Build.
The new 5hz gps module I added in the last post is working. Now, to tackle the biggest problem discovered during Deathpod3000’s maiden voyage, we need a way to get very fast heading updates during turns. While the faster refresh rate of the new gps will help, it is never going to give us accurate heading data in a turn. In order to do that we need a compass. In this post, I’ll describe how I added a digital i2c compass to Deathpod.
Even with 5hz updates, the gps isn’t going to be able to give us accurate heading data during a turn. The reason is a gps figures out heading based on a line drawn from where you were during the last update to where you are this update. In a turn, as the car goes around in a circle, the nose is always pointing beyond this line so the gps heading will always lag behind the true direction the car is pointing.
The way to get an accurate heading reading in a turn is with a compass. Our UART (regular serial port) is already being used by the gps, so we need a compass with some other interface. The Atmega32 has both spi and i2c synchronous serial. Sparkfun carries a digital compass chip with an i2c interface mounted on a small board with spots to solder pins at standard .1″ spacing. The Honywell HMC6352 mounted on a breakout board requires only 4 connections: 5v, GND, clock (SCL) and data (SDA).
I2c is a 2 wire synchronous serial interface. It is actually a bus since it can accomodate multiple slave devices and a master device on only 2 wires. I2c was originally developed by Philips and to get around licensing costs, other manufacturers have released their own implementations. Smbus is compatible with i2c and is used on many motherboards to communicate with temperature sensors. Atmel calls it TWI (Two Wire Interface).
The two wires that make up the i2c bus are clock and data. The master drives the clock and both master and slave transmit using the data line. In this case, the Atmega32 will be the master and the compass chip will be the slave. Because it is a synchronous interface, much higher transfer speeds are possible than with an async serial interface like rs232. The “slow” speed is 100 Kbps and you can go up to 1 Mbps with some devices. Because of the fast speed, small number of connections required and ability to connect multiple devices on the same bus, i2c is a very popular communication mechanism for microcontrollers. You can find devices that use i2c ranging from eeprom chips to sonars to motor controllers and compasses.
I soldered standard straight pins onto the compass breakout board thinking I’d use one of the 10 pin header ribbon cables I had to connect to similar pins on the final board. This will allow me to connect and disconnect the compass easily if I want to use it on another project in the future. On the Atmega32, pin PC0 is SCL and PC1 is SDA. I used breadboard wires to connect the 10 pin header to the correct pins on the STK500 dev board.
I2c handles multiple devices on a single 2 wire interface by assigning each device a unique 7 bit address. The compass by default uses address hex 42. The master device doesn’t need an address.
AVRlib includes a module to handle i2c communication both as a master and slave. We’ll use this module to communicate with the compass. The library makes this pretty easy and we will only need to call one fuction to write to the device and 1 function to read from the device. All the low-level stuff is taken care of by the library and the AVR hardware. By default, the compass is in “query” mode. It will take and return a reading on command. This sounds fine for what we want to do.
To take a reading you must first send hex 41, then read 2 bytes (MSB, LSB) of heading. The heading has an implied 1 decimal point so a reading of 2436 would be a heading of 243.6 degrees. It takes 6 ms for the compass to compute the heading so we’ll put in a little delay of 10ms to let it do it’s magic. First, in order to make changes easier, we’ll put in a define for the target address of our i2c commands.
//define for compass iic addresses
#define TARGET_ADDR 0x42
I put the code to read a compass heading in a function so it will be clean and easy to read the heading in our main processing loop. In order to account for errors in compass placement (where the compass isn’t pointing directly forward on the car), I included an offset passed to the function that we can use later if we need to correct for some heading error. Here is the code in my compass reading function.
float GetCompassHeading(float offset)
int reading = 0;
float returnHeading = 0.0;
c = 0x41;
i2cMasterSend(TARGET_ADDR, 1, &c);
i2cMasterReceive(TARGET_ADDR, 2, &in);
reading = in;
reading = reading << 8;
reading += in;
returnHeading = (float) reading;
returnHeading = returnHeading / 10;
returnHeading += offset;
if(returnHeading < 0)
returnHeading += 360;
if(returnHeading >= 360)
returnHeading -= 360;
No indenting for some reason. Still readable I guess. Anyway, set up unsigned char variable c to hold the “command” to send and 2 byte array in for the returned data. We’ll convert the reading to an in int called reading and use a float to hold the final reaturn heading. We set the command to 0x41 (hex 41 or the character ‘A’) and then call i2cMasterSend, the AVRlib command to transmit. The three parameters are i2c address, bytes to send and a pointer to the data. Next we wait 10ms and then read the result. i2cMasterReceive takes the same 3 parameters as send. We read the first byte (MSB), then shift it 8 bits to the left and then add the next byte we received to construct the 16 bit int. Next we convert it to a float and load it into our returnHeading, then divide by 10 to get our xxx.x heading.
Now we will deal with the offset passed into the function. First we simplyadd it to the heading. At this point, if the offset is negative and the heading is close to 0, we could end up with negative heading. For example, let’s say the heading is 006.0 and offset is -9. The returned heading would be -003.0, which doesn’t make any sense. To account for this, if the heading ends up less than 0, we add 360. So in our example, -003.0 + 360 = 357.0. In the same way, with a positive offset and a heading near 360, we could end up with a heading that greater than 360. We take care of this situation by subtracting 360 if the heading is greater than or equal to 360. Now we will always return a heading between 0 and 359.9 degrees.
I changed the main navigation loop to read the compass, use the compass heading for steering rather than the gps heading and display the compass heading on the LCD. Turning on the system gave me…nothing. I was getting 0 no matter which way I turned the compass. I searched the web, I double-check the code, I tried non-interrupt driven i2c calls. Then I checked the wiring. It turns out I had the SDA line connected to the wrong spot on the header. I moved the wire over one spot and bingo – rapidly changing heading readings.
The compass chip has a calibration mode where you spin the chip around for several seconds and it calibrates to it’s environment in case there is metal around, etc. I added an option when the system starts up to allow the user to do the calibration and then point to the first waypoint stored in eeprom. The car calculates an offset based on this and stores the offset to feed to the compass heading reading function.
Testing the new setup with the faster gps and digital compass proved successful. The car no longer drove around in circles, but once it figured out bearing to the waypoint, it drove straight toward it. The waypoint tended to move around because of gps error, but the car was able to track directly to where it thought the waypoint was.
Navigation is working. The next step is to ditch the STK500 dev board and wire up the final, much smaller and lighter board and start attaching things more permanently to the car.