Previously I’ve been examining how to use I2C devices with the Raspberry Pi 3, and developing drivers for these devices in C#. I’ve done this for a temperature sensor previously, and abstracted out some methods into a NuGet package which I hope can be re-used across other I2C projects.

In this post, I’ll develop some C# code to allow me to use the HMC5883L digial compass, and use the NuGet package I developed previously to help simplify and standardise the way the driver is developed.

I’ve previously managed to get the HMC5883L device working with the Netduino class of device. The code that I’ve written for the Raspberry Pi 3 is similar to the original code, but obviously has differences.

Special registers for the HMC5883L

There are a number of key pieces of information about the HMC5883L:

  • The I2C slave address – 0x1E
  • The register which holds the operating mode – 0x02
  • The first of 6 registers which holds the most significant byte (MSB) and least significant byte (LSB) for each of the X, Y and Z axes – 0x03

Also, there are three registers which hold information that can be used to uniquely identify the device:

  • Identification register A is at 0x0A and should contain the value 0x48
  • Identification register B is at 0x0B and should contain the value 0x34
  • Identification register C is at 0x0C and should contain the value 0x33

Writing the standard code for the device

The first thing is to install the I2C NuGet package which I wrote previously. This allows me to extend the AbstractI2cDevice class, and override some of the methods specified in this class.

public class HMC5883L : AbstractI2CDevice

Next, I need to declare the special registers which I mentioned in the previous section.

private const byte I2C_ADDRESS = 0x1E;
 
private byte OperatingModeRegister = 0x02;
 
private byte[] FirstDataRegister = new byte[] { 0x03 };
 
private byte[] IdentificationRegisterA = new byte[] { 0x0A };
 
private byte[] IdentificationRegisterB = new byte[] { 0x0B };
 
private byte[] IdentificationRegisterC = new byte[] { 0x0C };

I choose to declare a constructor which contains the information to uniquely identify the device, and I also have to override the abstract GetI2cAddress() method.

public HMC5883L()
{
    this.DeviceIdentifier = new byte[3] { 0x480x340x33 };
}
 
public override byte GetI2cAddress()
{
    return I2C_ADDRESS;
}

One more method that I need to override is GetDeviceId() – this queries the identification registers.

public override byte[] GetDeviceId()
{
    var identificationBufferA = new byte[1];
    var identificationBufferB = new byte[1];
    var identificationBufferC = new byte[1];
 
    this.Slave.WriteRead(IdentificationRegisterA, identificationBufferA);
    this.Slave.WriteRead(IdentificationRegisterB, identificationBufferB);
    this.Slave.WriteRead(IdentificationRegisterC, identificationBufferC);
 
    return new byte[3] { identificationBufferA[0], identificationBufferB[0], identificationBufferC[0] };
}

Writing code specific to this device

The HMC5883L compass has a number of different operating modes, including continuous measurement, and single measurement, and idle modes. I created an enumeration to list these modes, and the hexidecimal values associated with each of these modes.

public enum OperatingMode
{
    CONTINUOUS_OPERATING_MODE = 0x00,
 
    SINGLE_OPERATING_MODE = 0x01,
 
    IDLE_OPERATING_MODE = 0x10
}

The operating mode is specified by writing these enumeration values to the OperatingModeRegister specified in the member variable section above.

public void SetOperatingMode(OperatingMode operatingMode)
{
    // convention is to specify the register first, and then the value to write to it
    var writeBuffer = new byte[2] { OperatingModeRegister, (byte)operatingMode };
 
    this.Slave.Write(writeBuffer);
}

Finally, I need to get the 6 bytes of data which give information about each of the three axes. The X, Y, and Z directions are each specified as two bytes, so there are 6 bytes of compass data in total. Each of these directions can be specified as an integer by adding the two bytes. I find the easiest way to represent these three axes is as a struct.

public struct RawData
{
    public int X { getset; }
    public int Y { getset; }
    public int Z { getset; }
}

And to get these 6 bytes of direction information, we simply read 6 bytes from the contents of the first data register into an empty 6 byte array.

var compassData = new byte[6];
 
this.Slave.WriteRead(FirstDataRegister, compassData);

In order to get the raw directional data from the three byte pairs of data, I shift the MSB by 8 bits and perform a logical OR operation with the LSB. I can then combine it to a 16-bit signed integer.

var xReading = (short)((compassData[0<< 8| compassData[1]);

Now it’s just a simple case of assigning these values into the RawData struct. The full method is shown below:

public RawData GetRawData()
{
    var compassData = new byte[6];
 
    this.Slave.WriteRead(FirstDataRegister, compassData);
 
    var rawDirectionData = new RawData();
 
    var xReading = (short)((compassData[0<< 8| compassData[1]);
    var zReading = (short)((compassData[2<< 8| compassData[3]);
    var yReading = (short)((compassData[4<< 8| compassData[5]);
 
    rawDirectionData.= xReading;
    rawDirectionData.= yReading;
    rawDirectionData.= zReading;
 
    return rawDirectionData;
}

The full code is on GitHub at this link.

Using the HMC5883L

I connected the HMC5883L directly to my Raspberry Pi 3, using 4 connectors:

  • 5v to Pin 4
  • Ground to Pin 6
  • SCL (serial clock) to Pin 5
  • SDA (serial data) to Pin 3

Now by creating a new UWP app for Windows 10, and including the Windows IOT Extensions through Visual Studio and referencing the HMC5883L project, the compass chip can now be used with the sample code below.

private async Task WriteCompassSettingsToDebug()
{
    var compass = new HMC5883L();
 
    await compass.Initialize();
            
    if (compass.IsConnected())
    {
        compass.SetOperatingMode(OperatingMode.CONTINUOUS_OPERATING_MODE);
 
        while (true)
        {
            var direction = compass.GetRawData();
 
            Debug.WriteLine($"X = {direction.X}, Y = {direction.Y}, Z = {direction.Z}");
                    
            Task.Delay(1000).Wait();
        }
    }
}