Arduino + HMC6343 + u-blox MAX-6

Part of my investigation into simultaneous presence in real & virtual environments involves streaming real world position, heading & orientation data into a modified Second Life viewer. After much faffing about evaluation of different accelerometers, magnetometers & GPS receivers, as well as learning about GNSS & exciting concepts like magnetic declination, hard/soft iron offsets & tilt compensation, the result is an Arduino with an HMC6343 tilt-compensated magnetometer & a u-blox MAX-6 GPS receiver.

For the interaction style that I want to investigate with the VTW project I need to know 3 things about the user;

  • position – where they are
  • heading – the direction they are looking
  • pitch – the angle they are facing (up, down, level)

Pitch is the easiest of the three – you simply use an accelerometer. I tried both the ADXL335 with signal conditioned voltage outputs & the MMA8452 which uses I2C. These are both 3-axis accelerometers so can tell you roll, pitch & yaw all at the same time. They both did what I wanted, but the MMA8452 was easier to use as it didn’t involve experimenting to find out what values represented the rest/peak positions of each axis like the ADXL335 did.

Heading (by which I mean a compass heading) also seems easy to begin with – you just use a 3-axis magnetometer (aka ‘digital compass’ or ‘e-compass’) & perform a calculation that uses the readings of the 2 axes that represent pitch & roll. I tried the HMC5883L but soon discovered that things get much more complicated if you aren’t holding the magnetometer flat – because the calculation doesn’t take the third axis into consideration, the heading becomes more useless the more you tilt it! To calculate a heading even when not held level (which is important for many applications) you need to integrate the reading from the third axis, which can be thought of as recording the magnetic field lost by the other two when tilted out of alignment, into the heading calculation. But to do this you need to know how the magnetometer is tilted – good thing we have a 3-axis accelerometer!

There are a number of good tutorials/guides for this, including these two that I followed, however if you are lazy &/or scared by maths you can also buy ICs such as the HMC6343 that combine a 3-axis accelerometer with a 3-axis magnetometer & perform the tilt compensation algorithms onboard.

Of course there must be a downside to this convenience & most obvious is the price. If you are a SparkFun sort of person the HMC6343 will set you back $149.95. To put that into perspective the ADXL335 is $24.95, the MMA8452 a paltry $9.95 & the HMC5883L just $14.95.

The HMC6343

The HMC6343 ‘3-Axis Compass with Algorithms’ is an I2C device so is easy to hook up (SDA to analog 4, SCL to analog 5 on an Arduino Uno) & interact with. The datasheet contains the protocol definition & communicating with it looks something like this (the example here changes the rate of measurements from the default of 5Hz to the fastest of 10Hz). HMC6343_ADDRESS is 0x19 & obviously you will also need to include Wire.h at the top of your sketch.

Wire.beginTransmission(HMC6343_ADDRESS);
Wire.write(0xF1);
Wire.write(0x05);
Wire.write(0x02);
Wire.endTransmission();

The first Wire.write tells the device what command we want to run, in this example 0xF1 is the ‘Write to EEPROM’ command that expects 2 binary byte arguments. The first argument is which register we want to write to, in this example 0x05 is the address of ‘Operational Mode Register 2’ which stores the measurement speed. The second argument is the value that we actually want to write to this register & in this case 0x02 means that we want measurements at 10Hz. Simple enough, no?

If you look through the sketch you’ll see this pattern repeated for setting the variation angle correcton, the Infinite Impulse Response filter & temporarily changing the orientation (so that you can mount the IC in different orientations without having to manually swap the axes, which is very handy).

The HMC6343 also allows you to access just the accelerometer readings, separately from the tilt-compensated magnetometer readings, so this single IC gives me both the (tilt compensated) heading & the pitch.

One more handy feature of the HMC6343 is the hard-iron offset calibration. Metallic stuff around the chip (wires, the GPS module, the Arduino, etc.) can affect the magnetic field detected by the magneto resistors & throw off the readings, so we can compensate for this using the built-in calibration mode. Read the datasheet to see how this works, but essentially all you have to do is send command 0x71, rotate the device in a certain manner, then send command 0x7E.

In case you’re wondering what the difference between hard-iron offset & soft-iron offset is, the former is for things that won’t change during operation (eg the wires connecting the IC to the Arduino are always going to be there) whereas the latter refers to things that will change during operation (such as moving the IC around your lodestone collection). Hard-iron only need be calculated once, whereas soft-iron requires dynamic attention.

u-blox MAX-6

My research into GNSS led me to the u-blox MAX-6 as my best option. Impressive specification, solid performance & compatibility with SBAS including EGNOS in Europe (Wikipedia page on GNSS augmentation/SBAS here). It turns out that it’s a popular GPS receiver amongst high-altitude ballooners (the kind of people who attach Arduinos & cameras to weather balloons & let them go) as it operates correctly at high altitudes which some receivers evidently don’t. So after some discussion on the #highaltitude channel on freenode I ended up ordering a MAX-6 with the appropriate level conversion to work with an Arduino (the MAX-6 uses 3v logic, whereas the Arduino uses 5v, so conversion is required so as not to fry the module). I ordered it from here & based my sketch on the example code here.

The MAX-6 is a serial device (no convenient I2C here) so interfacing with an Arduino is slightly different. The wiki page linked above recommends not using Software Serial, however as I also want to run the HMC6343 on the same Arduino I have no choice – so far I haven’t experienced any trouble with it.

The protocol specification for the MAX-6 is substantially longer & the messages much more complex than the HMC6343. If you want to have a go at assembling the messages by hand you can do so by reading the 220+ page document & I actually had some success with this, however then discovered that the easier way is to use the u-center software which allows you to configure the receiver from Windows using tickboxes & menus then see/copy the messages from the relevant console window to paste into your sketch. The u-center software worked fine for me in a Windows 7 VM running on a Linux host, using pins 0 & 1 on an Arduino to connect the receiver directly to the USB connection via the Arduino’s UART.

Once you have your message, sending it to the receiver looks something like this (the example here sets the dynamic platform model to ‘pedestrian’);

uint8_t CFG_NAV5[] = {0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF,
                    0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27,
                    0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00,
                    0x64, 0x00, 0x2C, 0x01, 0x32, 0x3C, 0x00, 0x00,
                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                    0x00, 0x00, 0x00, 0x00};
calculateUBXChecksum(CFG_NAV5, (sizeof(CFG_NAV5)/sizeof(uint8_t)));

while (!success)
{
  sendUBX(CFG_NAV5, (sizeof(CFG_NAV5)/sizeof(uint8_t)));
  success = getUBX_ACK(CFG_NAV5);
}

The first thing we do is create an array of type unit8_t that contains the message itself. The final 2 hex values are the checksum & the call to calculateUBXChecksum on line 2 calculates these values & substitutes them into the array. To send the message we use the sendUBX method & keep on trying until the receiver sends us a confirmation.

Again you will see this pattern repeated a number of times in the sketch, to set the dynamic platform model, enable SBAS using EGNOS, disable all the messages that I don’t want & enable the ones that I do want.

Getting readings

Getting data from the HMC6343 is a simple case of sending the correct command (0x50), requesting the correct number of bytes & then reading them into sensible variables (heading, pitch, etc.).

Getting data from the MAX-6 isn’t much harder, using SoftwareSerial.available() & SoftwareSerial.Read(), however as with most GPS receivers the data that it returns is in the form of NMEA 0183 messages which look a bit like this;

$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68

It’s not hard to parse it yourself, but why go to the effort when there are libraries like TinyGPS that can do it for you?

…using readings?

So what was the point of all this again? Well, my modified Second Life client listens on the serial port using Boost.Asio, receives these messages & then uses then to control avatar & camera – more on this soon!

Arduino + accelerometer as joystick for Second Life

Update – example code available here.

Here’s something I hacked together earlier this year & although I didn’t end up developing it further it may prove interesting to others.


First off a quick video showing the Arduino + accelerometer ‘joystick’ controlling the avatar & then the flycam in the official Second Life client. If this piques your interest, read on to find out how (& why) it was done.

I wanted to investigate how I might use real world orientation data, such as that recorded by an accelerometer attached to an Arduino, to control a Second Life avatar without having to modify the source code of the Second Life client. This approach would have two major advantages;

  • conceivably much less work required
  • compatibility with any Second Life client sans modification – no reliance on a bespoke modified client

Unfortunately the official Second Life client (upon which all third party clients are based) doesn’t really present any interfaces for input devices apart from the usual mouse, keyboard & joystick. But some work that my colleague John was doing at the time getting XBox controllers to move avatars using the joystick interface got me thinking & a Google search for ‘Arduino joystick’ led to my discovery of the Arduino UNO Joystick HID firmware based on LUFA (the Lightweight USB Framework for AVRs) which allows an Arduino to appear to a computer as a standard USB HID joystick, instead of as a serial device, by reprogramming the USB-to-serial converter. Of course this ‘joystick’ can in fact be used by any program/game, not just Second Life.

Note: this is only possible with Arduino Uno & Mega, which use an ATMega chip for USB-to-serial conversion. It does not work with older Arduino, such as the Duemilanove, which use a FTDI chip. My experiments used an Uno R3 which has the ATMega16U2, check compatibility before you attempt the following with a Uno/Mega R1/R2 which use the ATMega8U2.

Reprogramming the ATMega16U2 involves putting the Arduino into DFU mode & using dfu-programmer. Unfortunately the latest version of dfu-programmer (0.5.4) doesn’t know about the ATMega16U2 so has to be patched. Grab the latest dfu-programmer source & apply this patch as discussed in this thread on the Arduino.cc forums.

With your Arduino connected to your computer as normal you should see something similar if you run lsusb.

[root@flatline /]# lsusb
Bus 003 Device 007: ID 2341:0043 Arduino SA Uno R3 (CDC ACM)

To enter DFU mode find the 6-pin AVR header near the USB socket & briefly connect the 2 pins closest to the USB socket (see picture beneath, click for full size) using the tip of a screwdriver, a paperclip, piece of wire, etc.

The Arduino should no longer register in lsusb but a myserious Atmel Corp. device should have appeared in its place. The serial port (eg /dev/ttyACM0) should also have disappeared.

[root@flatline /]# lsusb
Bus 003 Device 011: ID 03eb:2fef Atmel Corp.

You can then go ahead & erase the original firmware & flash the joystick HID firmware.

[root@flatline /]# dfu-programmer atmega16u2 erase
root@flatline /]# dfu-programmer atmega16u2 flash Arduino-joystick-0.1.hex 
Validating...
4076 bytes used (33.17%)
[root@flatline /]# dfu-programmer atmega16u2 reset

And you’re done! At this point you will need to physically disconnect & reconnect the Arduino for the computer to recognise it as a joystick. After you have done so, lsusb should report something like this.

[root@flatline /]# lsusb
Bus 003 Device 012: ID 03eb:2043 Atmel Corp. LUFA Joystick Demo Application

With the joystick firmware in place you will no longer be able to upload sketches as normal. If you don’t have a USBtiny ISP or similar you will have to revert back to the original USB-to-serial firmware each time you want to upload a new sketch. This process is exactly the same as above, but substituting the joystick .hex file with the USB-to-serial one.

[root@flatline /]# dfu-programmer atmega16u2 erase
[root@flatline /]# dfu-programmer atmega16u2 flash Arduino-usbserial-atmega16u2-Uno-Rev3.hex 
Validating...
4034 bytes used (32.83%)
[root@flatline /]# dfu-programmer atmega16u2 reset

Once again, you will have to disconnect & reconnect the Arduino for your computer to register the change.


As for the sketch itself, mapping accelerometer readings to the joystick axes is simply a case of inserting them into the correct variables in the joyReport struct & sending it over Serial – take a look at the example sketch that comes with the joystick firmware & you should soon see how to do it. Beneath is a rudimentary example using readings from the Honeywell HMC6343 from Sparkfun (see here for how to use this with Arduino), mapping the accelerometer’s roll to the joystick’s X axis & its pitch to the Y axis.

 
#include <Wire.h>
 
#define HMC6343_ADDRESS 0x19
#define HMC6343_HEADING_REG 0x50

// data structure as defined by the joystick firmeware
struct {
    int8_t x;
    int8_t y;
    uint8_t buttons;
    uint8_t rfu;
} joyReport;

void setup() {
  Wire.begin();          // initialize the I2C bus
  Serial.begin(115200);  // initialize the serial bus
}
 
void loop() {
  byte highByte, lowByte;
 
  Wire.beginTransmission(HMC6343_ADDRESS);    // start communication with HMC6343
  Wire.write(0x74);                           // set HMC6343 orientation
  Wire.write(HMC6343_HEADING_REG);            // send the address of the register to read
  Wire.endTransmission();
 
  Wire.requestFrom(HMC6343_ADDRESS, 6);       // request six bytes of data from the HMC6343
  while(Wire.available() < 1);                // busy wait while there is no byte to receive
 
  highByte = Wire.read();
  lowByte = Wire.read();
  float heading = ((highByte << 8) + lowByte) / 10.0; // heading in degrees
 
  highByte = Wire.read();
  lowByte = Wire.read();
  float pitch = ((highByte << 8) + lowByte) / 10.0;   // pitch in degrees
 
  highByte = Wire.read();
  lowByte = Wire.read();
  float roll = ((highByte << 8) + lowByte) / 10.0;    // roll in degrees

  joyReport.buttons = 0;
  joyReport.rfu = 0;
  
  joyReport.x = constrain(((int)(map(roll, -90, 90, -100, 100))), -100, 100);
  joyReport.y = constrain(((int)(map(pitch, -90, 90, -100, 100))), -100, 100);
  
  Serial.write((uint8_t *)&joyReport, 4);

  delay(100); // do this at approx 10Hz
}

When you fire up your Second Life client (either the official client or a third-party client) go into Preferences -> Move & View -> Other Devices to open the Joystick Configuration window & you should see something like the screenshot beneath (click for full size). Note ‘Arduino Arduino Joystick’ has been recognised – however also note that it is a limitation of the client that it only recognises the first joystick device connected to the computer. Depending on how you have mapped your axes & what you want to control you will have to change the numbers in this window accordingly – with the above sketch 0 is the X axis & 1 is the Y axis (-1 disables a control).


In the end this approach proved to be unsuitable for my purposes, due to the difficulty of mapping readings to discrete virtual world orientations rather than to relative movements from the previous orientation. But it was still interesting to do :)