Reading and Setting the I2C Address on an HMC6352 Compass Module

Hi everyone, I'm Matt, and this is my first post on this forum. It's also my first non-tutorial Arduino project, and I'm pleased to report good progress and many successes, along with several good lessons learned. Even though some of this might be obvious to many of you, I had a bit of a learning curve with it as a first-timer. I'm here to share what I found out, in order to help the next guy.

First, if you want to use the HMC6352 compass module with the Arduino, it's not hard, but it communicates using the Inter-Integrated Circuit (I2C) bus, which I learned is essentially a popular protocol ICs use to talk to each other. It uses a data line (SDA) and a clock line (SCL), and all devices on the bus share the same 2 pins on the board. SDA uses analog pin 4, and SCL uses analog pin 5 when the devices are connected to an Arduino (Duemillanove). Because all devices share the same physical connection, they are identified by their I2C Slave Address so that the data moves between the master (micro-controller) and it's various slave devices correctly. It's pretty easy to find a code sample that shows you how to get a heading out of the HMC6352 compass module with the Arduino using it's default I2C address, 0x42 (hexidecimal).

That worked pretty much right out of the box for me, but the device I'm building will use two different compass modules so that I can find the degrees of deviation between one compass heading and another (the direction my head is facing relative to my torso). Because of this, I cannot use the default address for one of the two devices... the two slave addresses on the bus have to be different. I have to find and change the address in one HMC6352. I didn't find much online about how to do that with the Arduino, but I found the data sheet http://www.sparkfun.com/datasheets/Components/HMC6352.pdf and found that it is possible to send commands that will read and write to the EEPROM (Electrically Erasable Programmable Read Only Memory) where the address is stored on the sensor.

I played around with the code, spent a while debugging it, and came up with the following, which allowed me to correctly read the address value from the EEPROM:

//Checking the I2C address of a HMC6352 Sparkfun Compass Module with Arduino 
// By Matt Silvia 01/29/2012
#include <Wire.h>
int HMC6352Address = 0x42;
byte eepromAddress = 00; // the address in the HMC6352 EEPROM from which to request the value of the I2C Slave Address
int slaveAddress;
byte addressData[1];
int addressValue;
void setup()
{
//  Shift the device's documented slave address (0x42) 1 bit right
//  This compensates for how the TWI library only wants the
//  7 most significant bits (with the high bit padded with 0)
slaveAddress = HMC6352Address >> 1;   // This results in 0x21 as the address to pass to TWI
Serial.begin(9600);
Wire.begin();
}
void loop()
{

  // Send a "r" command to the HMC6352
  // along with the EEPROM address to read
  Wire.beginTransmission(slaveAddress);
  Wire.send('r');             //read from EEPROM
  Wire.send(eepromAddress);   //argument to the 'r' command is sent seperately
  Wire.endTransmission();
 
  Wire.requestFrom(slaveAddress, 1);        // Request the address
  addressData[0] = Wire.receive();

  Serial.print(addressData[0], HEX); 
  Serial.println(" is the address.");  

  
  delay(500);
}

Serial out reports that 42 is the address, so I believe I'm only a stone's throw from setting that to another value by using the "w" command with very similar code. When I get it working, I'll try to remember to come back and post the code for changing the value, as well as the final code for the degrees of variation between two compasses.

It seems to have worked! I modified that code so that it would write a new address value to the EEPROM before reporting it back, and was able to change the address from the factory default 0x42 to 0x40. After unplugging and restoring power to my circuit, I tried my read-only address sketch again, and instead of 42, it reported 0... a good sign, since it was still trying to talk to the device at 0x42.

//Setting a new I2C address on a HMC6352 Sparkfun Compass Module with Arduino 
// Once the new address is written to the EEPROM, the address will be assigned to the device when the power is reset.
// By Matt Silvia 01/29/2012
#include <Wire.h>
int HMC6352Address = 0x42;
int newAddress = 0x40; //the new address to be assigned to the HMC6352
byte eepromAddress = 00; // the address in the HMC6352 EEPROM from which to request the value of the I2C Slave Address
int slaveAddress;
byte addressData[1];
int addressValue;
void setup()
{
//  Shift the device's documented slave address (0x42) 1 bit right
//  This compensates for how the TWI library only wants the
//  7 most significant bits (with the high bit padded with 0)
slaveAddress = HMC6352Address >> 1;   // This results in 0x21 as the address to pass to TWI
Serial.begin(9600);
Wire.begin();
}
void loop()
{

  // Send a "r" command to the HMC6352
  // along with the EEPROM address to read
  Wire.beginTransmission(slaveAddress);
  // Write the new address
  Wire.send('w');             //write to EEPROM
  Wire.send(eepromAddress);   //agruments are sent seperately
  Wire.send(newAddress);      //2nd argument is the new address to be written.
  delay(10);
  // Read the address to confirm
  Wire.send('r');             //read from EEPROM
  Wire.send(eepromAddress);   //argument to the 'r' command is sent seperately
  Wire.endTransmission();
 
  Wire.requestFrom(slaveAddress, 1);        // Request the address
  addressData[0] = Wire.receive();

  Serial.print(addressData[0], HEX); 
  Serial.println(" is the address.");  

  
  delay(500);
}

I loaded my previously-working compass heading sketch, changed the address to 0x40, and got good data back. XD

Now... on to the two compass circuit. I understand I'll have to use a couple of 10 Kohm pick-up resistors, but I read up on how those work and while I don't completely understand how they work, I get the gist and am no longer concerned about connecting the SDA and SCL lines to power. That made me a little uneasy at first. Hopefully that will work smoothly.

I'm now getting headings from two HMC6352 modules in the serial monitor. XD

// 2 HMC6352 Sparkfun Compass Modules and Arduino 
// by Matt Silvia 1/29/2012
// based on HMC6352 Sparkfun Compass Module and Arduino · posted by vaibhav bhawsar Jul 16, 22:42
#include <Wire.h>
int HMC6352Address1 = 0x40; // This assumes you have changed the default adderss of one compass module to 0x40
int HMC6352Address2 = 0x42; // This is the default address value
// This is calculated in the setup() function
int slaveAddress1;
int slaveAddress2;
byte headingData1[2];
byte headingData2[2];
int i, headingValue1, headingValue2;
void setup()
{
// Shift the devices' documented slave addresses (0x40, 0x42) 1 bit right
// This compensates for how the TWI library only wants the
// 7 most significant bits (with the high bit padded with 0)
slaveAddress1 = HMC6352Address1 >> 1;   // This results in 0x21 as the address to pass to TWI
slaveAddress2 = HMC6352Address2 >> 1;   
Serial.begin(9600);

Wire.begin();
}
void loop()
{
  // Send a "A" command to the first HMC6352
  // This requests the current heading data
  Wire.beginTransmission(slaveAddress1);
  Wire.send("A");              // The "Get Data" command
  Wire.endTransmission();
  delay(10);                   // The HMC6352 needs at least a 70us (microsecond) delay
                               // after this command.  Using 10ms just makes it safe
  
  // Read the 2 heading bytes, MSB first
  // The resulting 16bit word is the compass heading in 10th's of a degree
  // For example: a heading of 1345 would be 134.5 degrees
  Wire.requestFrom(slaveAddress1, 2);        // Request the 2 byte heading (MSB comes first)
  i = 0;
  while(Wire.available() && i < 2)
  { 
    headingData1[i] = Wire.receive();
    i++;
  }
  headingValue1 = headingData1[0]*256 + headingData1[1];  // Put the MSB and LSB together
  
  // Send a "A" command to the second HMC6352
  // This requests the current heading data
  Wire.beginTransmission(slaveAddress2);
  Wire.send("A");              // The "Get Data" command
  Wire.endTransmission();
  delay(10);                   // The HMC6352 needs at least a 70us (microsecond) delay
  // after this command.  Using 10ms just makes it safe
  // Read the 2 heading bytes, MSB first
  // The resulting 16bit word is the compass heading in 10th's of a degree
  // For example: a heading of 1345 would be 134.5 degrees
  Wire.requestFrom(slaveAddress2, 2);        // Request the 2 byte heading (MSB comes first)
  
  i = 0;
  while(Wire.available() && i < 2)
  { 
    headingData2[i] = Wire.receive();
    i++;
  }
  headingValue2 = headingData2[0]*256 + headingData2[1];  // Put the MSB and LSB together
  
  Serial.print("Current heading for compass 1: ");
  Serial.print(int (headingValue1 / 10));     // The whole number part of the heading
  Serial.print(".");
  Serial.print(int (headingValue1 % 10));     // The fractional part of the heading
  Serial.println(" degrees");
  
  Serial.print("Current heading for compass 2: ");
  Serial.print(int (headingValue2 / 10));     // The whole number part of the heading
  Serial.print(".");
  Serial.print(int (headingValue2 % 10));     // The fractional part of the heading
  Serial.println(" degrees");
  delay(500);
}

Thanks for the detailed write up on this sensor. I plan to use a compass module soon, and I'd be interested in hearing how well it performs once it is installed.

-transfinite

There is a good I2C scanner on this page - Gammon Forum : Electronics : Microprocessors : I2C - Two-Wire Peripheral Interface - for Arduino - very usefull when doing I2C address magic ;).

transfinite:
I'd be interested in hearing how well it performs once it is installed.

So far, it seems to report fairly accurate headings within a few degrees. Even when stationary, the results may jump around a bit... 179.16, 180.65, 178.92, 179.87, etc., but it does seem close to true. The two units I have disagree slightly about where magnetic north is, but if that becomes a problem, it looks from the spec like it's possible to recalibrate it.

I think the offset angle calculation is working. It's set to report the degrees of difference from center with a minimum of -90 and maximum of 90 degrees. I haven't fully tested it yet, but it seems to work well. I was a bit tired when I finished it though, so a few glitches wouldn't surprise me.

// offset angle of 2 HMC6352 Sparkfun Compass Modules
// by Matt Silvia 1/29/2012
// based on HMC6352 Sparkfun Compass Module and Arduino · posted by vaibhav bhawsar Jul 16, 22:42
// This assumes you have changed the default address of one compass module to 0x40
#include <Wire.h>
int HMC6352Address1 = 0x42; // torso
int HMC6352Address2 = 0x40; // head
// This is calculated in the setup() function
int slaveAddress1;
int slaveAddress2;
byte headingData1[2];
byte headingData2[2];
int i, headingValue1, headingValue2, difference, offsetAngle;
void setup()
{
// Shift the devices' documented slave addresses (0x40, 0x42) 1 bit right
// This compensates for how the TWI library only wants the
// 7 most significant bits (with the high bit padded with 0)
slaveAddress1 = HMC6352Address1 >> 1;   // This results in 0x21 as the address to pass to TWI
slaveAddress2 = HMC6352Address2 >> 1;   
Serial.begin(9600);

Wire.begin();
}
void loop()
{
  // Send a "A" command to the first HMC6352
  // This requests the current heading data
  Wire.beginTransmission(slaveAddress1);
  Wire.send("A");              // The "Get Data" command
  Wire.endTransmission();
  delay(10);                   // The HMC6352 needs at least a 70us (microsecond) delay
                               // after this command.  Using 10ms just makes it safe
  
  // Read the 2 heading bytes, MSB first
  // The resulting 16bit word is the compass heading in 10th's of a degree
  // For example: a heading of 1345 would be 134.5 degrees
  Wire.requestFrom(slaveAddress1, 2);        // Request the 2 byte heading (MSB comes first)
  i = 0;
  while(Wire.available() && i < 2)
  { 
    headingData1[i] = Wire.receive();
    i++;
  }
  headingValue1 = headingData1[0]*256 + headingData1[1];  // Put the MSB and LSB together
  
  // Send a "A" command to the second HMC6352
  // This requests the current heading data
  Wire.beginTransmission(slaveAddress2);
  Wire.send("A");              // The "Get Data" command
  Wire.endTransmission();
  delay(10);                   // The HMC6352 needs at least a 70us (microsecond) delay
  // after this command.  Using 10ms just makes it safe
  // Read the 2 heading bytes, MSB first
  // The resulting 16bit word is the compass heading in 10th's of a degree
  // For example: a heading of 1345 would be 134.5 degrees
  Wire.requestFrom(slaveAddress2, 2);        // Request the 2 byte heading (MSB comes first)
  
  i = 0;
  while(Wire.available() && i < 2)
  { 
    headingData2[i] = Wire.receive();
    i++;
  }
  headingValue2 = headingData2[0]*256 + headingData2[1];  // Put the MSB and LSB together
  
  //Serial.print("Current heading for compass 1: ");
  //Serial.print(int (headingValue1 / 10));     // The whole number part of the heading
  //Serial.print(".");
  //Serial.print(int (headingValue1 % 10));     // The fractional part of the heading
  //Serial.println(" degrees");
  
  //Serial.print("Current heading for compass 2: ");
  //Serial.print(int (headingValue2 / 10));     // The whole number part of the heading
  //Serial.print(".");
  //Serial.print(int (headingValue2 % 10));     // The fractional part of the heading
  //Serial.println(" degrees");
  
  Serial.print("1: ");
  Serial.println(headingValue1);
  Serial.print("2: ");
  Serial.println(headingValue2);
  difference = ((headingValue2 - headingValue1) / 10);
  Serial.print("Difference: ");
  Serial.println(difference);
  
  // This section figures out the degrees of difference between the two headings, 
  // accounting for the change from 359 degrees to 0 degrees
  if ((difference > -91) && (difference < 91))
  {
    offsetAngle = difference;
  }
  else if (difference > 270)
  {
    offsetAngle = -(360 - (headingValue2 / 10)) + (headingValue1 / 10);
  }
  else if (difference < -270)
  {
    offsetAngle = (360 + (headingValue2 / 10)) - (headingValue1 / 10);  
  }
  else if ((difference > 90) && (difference < 180))
  {
    offsetAngle = 90;
  }
    else if ((difference > 181) && (difference < 271))
  {
    offsetAngle = -90;
  }
  else if ((difference < -90) && (difference > -180))
  {
    offsetAngle = -90;
  }
    else if ((difference < -181) && (difference > -271))
  {
    offsetAngle = 90;
  }
  else // debuggging condition
  {
    Serial.println("other condition");
    offsetAngle = 0;
  }
  
  Serial.print("Offset Angle: ");
  Serial.println(offsetAngle);
  Serial.println( " ");
  delay(500);
}