Baby O driven HM55B compass module

I have finished coding the reader for an HM55B and thought some of you may find it helpful. There was a good bit if information for using this compass module with Arduinos and Basic Stamps, but not much for Orangutans. So here is my solution…

/*
   READ COMPASS ANGLE FROM THE PARALLAX HM55B DIGITAL COMPASS WITH THE POLOLU BABY ORANGUTAN 328P
   MAJORITY OF PROGRAM LOGIC: kiilo@kiilo.org WHO WROTE IT FOR THE ARDUINIO
   MODIFIED TO RUN ON THE BABY O BY ME: scottfromscott@bellsouth.net
   SEE THE flash_angle() FUNCTION FOR AN EXPLANATION OF HOW THE RED USER LED FLASHES THE CURRENT ANGLE 

   REFERENCES:
   http://www.parallax.com/Portals/0/Downloads/docs/prod/compshop/HM55BModDocs.pdf -- compass documentation
   http://www.parallax.com/Store/Sensors/CompassGPS/tabid/173/ProductID/98/List/0/Default.aspx?SortField=ProductName,ProductName -- product page
   http://arduino.cc/playground/HM55B -- killo's Arduino version
   https://forum.pololu.com -- general orangutan help
   https://www.pololu.com/docs/0J14 -- Baby O user's guide
   https://www.pololu.com/docs/pdf/0J20/pololu_avr_library.pdf
   https://www.pololu.com/docs/pdf/0J18/avr_library_commands.pdf
*/

#define F_CPU 20000000UL	// Baby Orangutan frequency (20MHz)

#include <math.h> 
#include <avr/io.h>
#include <util/delay.h> // uses F_CPU to achieve us and ms delays
#include <pololu/orangutan.h>
#include <stdlib.h>

// data connections:
//
// BABY O PINS <==> HM55B PINS
//  
//   PB0:IO_B0 <==> #4:CLK
//   PB1:IO_B1 <==> #5/Enable
//   PB2:IO_B2 <==> #1:Dout,#2:Din

// orangutan library defines used:
//
// for set_digital_output(), LOW, HIGH
// for set_digital_input() -- PULL_UP_ENABLED - enable pull-up resistor

float compass_angle = 999; // global variable available for other modules use

// for calibration: print out the compass 16-segment compass template at
// http://www.parallax.com/Portals/0/Downloads/docs/prod/compshop/HM55BModDocs.pdf
// line up north with a pocket compass and tape the template to a table
// this compass outputs negative angles for all westerly directions (180..359 -> -180..-1)
// so label the template as follows:
// 180(-180), 202.5(-157.5), 225(-135), 247.5(-112.5), 270(-90), 292.5(-67.5), 315(-45), 337.5(-22.5)
// set the Baby O powered HM55B in position facing south on compass template
// set calibrate_mode below to 1, then build and run this program
// you will now determine the compass reading and offset for each of 16 angles:
// starting with 180 degrees, read and write down the template angle, the compass reading
// and the offset. Repeat for all 16 angles. The offset for the reading will be a + or -
// number added to the reading to match the template angle-- eg. for a template angle of -90,
// where the reading is -115, the offset would be 25 -- -90 = -115 + 25
// now set calibrate_mode to 1 and complete the procedure above.
//
// Next, enter these values in the raw[] and offsets[] arrays in the corrected() function below
// the raw[] array is filled left to right, in negative to positive angles order
// the arrays are linked by their indices -- eg. offsets[2] goes with raw[2]
// in addition to the 16 values you just entered in each array above, you must do the following:
// add -180 at the beginning and 180 at the end of the raw[] array declaration;
// eg.    =>   int raw[18] = {-180, ...(your 16 values)..., 180}; 
// you must also add corresponding offset values for these to the offsets[] array
// which can easily be calculated with any spreadsheet program as follows:
// the offset for for -180 degrees...
// col A: template angle; col B: reading; col c: offset
// A1: 157.5, B1: (reading for 157.5), C1: (offset)  
// B2: -180, C2: =(B2-B1)*(C3-C1)/(B3-B1)+C1
// A3: -180, B3: (reading for -180), C3: (offset)
// 
// the offset for for 180 degrees...same as above but change B2 to 180
//
// so if offset for -180 is -4.2 and -23 for 180, then your offsets array declaration would look as follows
// eg. => int offsets[18] = {-4.2, ...(your 16 offsets)..., -23};
// 
// once your arrays are complete you may reset calibrate_mode to 0 below
// and rebuild and test your completed program which should more accurately
// display the compass angles
   
int calibrate_mode = 0; //0 = use linear interpolation tables; 1 = flash raw angle values for calibration

//// FUNCTIONS

void delayms( uint16_t millis )
{
   while(millis)
   {
      _delay_ms(1);
      millis--;
   }
}

void ShiftOut(int Value, int BitsCount)
{
   for(int i = BitsCount; i >= 0; i--)
   {
      set_digital_output(IO_B0, HIGH);
      if ((Value & 1 << i) == ( 1 << i)) set_digital_output(IO_B2, HIGH); // 1
      else set_digital_output(IO_B2, LOW); // 0 
      set_digital_output(IO_B0, LOW);
      delayMicroseconds(1);
   }
}

int ShiftIn(int BitsCount)
{
   int ShiftIn_result, i;
   ShiftIn_result = 0;
   set_digital_input(IO_B2, PULL_UP_ENABLED);
   for(i = BitsCount; i >= 0; i--)
   {
      set_digital_output(IO_B0, HIGH);
      delayMicroseconds(1);
      if (is_digital_input_high(IO_B2)) ShiftIn_result = (ShiftIn_result << 1) + 1;
      else ShiftIn_result = (ShiftIn_result << 1) + 0;
      set_digital_output(IO_B0, LOW);
      delayMicroseconds(1);
   }
   if ((ShiftIn_result & 1 << 11) == 1 << 11) ShiftIn_result = (0B11111000 << 8) | ShiftIn_result;
   return ShiftIn_result;
}

void setup()
{
   set_digital_output(IO_B1, HIGH); // not enabled initially
   set_digital_output(IO_B0, LOW); // clock is low initially
}

void HM55B_Reset()
{
   set_digital_output(IO_B1, LOW);
   ShiftOut(0B0000, 3); // send nibble of zeros
   set_digital_output(IO_B1, HIGH);
}

void HM55B_StartMeasurementCommand()
{
   set_digital_output(IO_B1, LOW);
   ShiftOut(0B1000, 3);
}

int HM55B_ReadCommand()
{
   int result = 0;
   set_digital_output(IO_B1, HIGH);
   set_digital_output(IO_B1, LOW);
   ShiftOut(0B1100, 3);
   result = ShiftIn(3);
   return result;
}

float get_compass_angle()
{
   float X_Data = 0, Y_Data = 0, angle = 0; // initialize to avoid compiler warning
   int HM55B_result; // status flag

   HM55B_StartMeasurementCommand(); // necessary!!
   delayms(40); // the data is 40ms later ready
   HM55B_result = HM55B_ReadCommand();
   while(HM55B_result != 0B1100) HM55B_result = HM55B_ReadCommand(); // poll until ready status
   X_Data = ShiftIn(11); // Field strength in X
   Y_Data = ShiftIn(11); // and Y direction
   set_digital_output(IO_B1, HIGH); // ok deselect chip
   angle = 180 * (atan2(-1 * Y_Data , X_Data) / M_PI);
   return(angle);
}
   
// I added the next 2 modules to use the red LED to aid reading and calibrating the compass
// If you don't need them, delete them and flash_angle() and delayms() references from main()

void flash_digit_value(int digit)
{
   // flash LED 'digit value' times for each digit in angle
   int counter;

   if(digit==0) digit = 10; // special case for 0
   
   for(counter=1;counter<=digit;counter++)
   {
      set_digital_output(IO_D1, HIGH); // LED on
	  delayms(200);	
      set_digital_output(IO_D1, LOW); // LED off
	  delayms(200);	
   }
   delayms(1000); // indicates end of digit
}

void flash_angle(float angle)
{
   // LED flashes indicate angle in degrees 0..359 by default
   // 1 flash -> 1, 2 flashes -> 2, .. 9 flashes -> 9, BUT 10 flashes -> 0 (special case)
   // ex. angle = 037 degrees -> 10 flashes + pause + 3 flashes + pause + 7 flashes + pause
   // ex. angle = -037 degrees (same as above but followed by 1 flash for - sign)
   // (readings repeat every 6 seconds)

   int ones, tens, hundreds;
   int uangle = abs(angle);
   hundreds = (int)(uangle/100);
   tens = (int)((uangle - hundreds*100)/10);
   ones = uangle - tens*10 - hundreds*100;
   flash_digit_value(hundreds);
   flash_digit_value(tens);
   flash_digit_value(ones);
   if(angle<0) flash_digit_value(1); // indicate negative angle
}

float adjusted(float myangle)
{
   // adjust raw angle reading by adding an offset 
   // these are values from my testing, yours will be different
   // 16 offsets are determined by comparing template angles with HM55B readings; the first and last offsets
   // are determined by linear interpolation
   //                 { -180, ... your 16 readings here ..., 180); 
   float raw[18] =    {  -180,-175, -152, -137,-126,-115,-106,-99,  -89, 9,   68, 90,  100,111,  125,141, 164,  180};
   float offsets[18] ={ -4.98,  -5, -5.5,    2,13.5,  25,38.5, 54, 66.5,-9,-45.5,-45,-32.5,-21,-12.5, -6,-6.5,-5.83};
   //                  {-4.98, ..., -5.83} these two values computed with spreadsheet   
   int index = 0;
   float offset;

   for(index=17;index>0;index--) if(myangle>=raw[index-1]) break;

   // do linear interpolation...

   offset = (myangle-raw[index-1])*(offsets[index]-offsets[index-1])/(raw[index]-raw[index-1])+offsets[index-1];

   if(calibrate_mode) return(myangle); // return raw angle for calibration
   else return(myangle+offset<0?myangle+offset+360:myangle+offset); // return calibrated angle 000..359
}

int main()
{
   int reading;
   while(1)
   {
      compass_angle = 0;
      for(reading=1;reading<=100;reading++) //get 100 readings, then average...
	  {
	     setup();
         HM55B_Reset();
         compass_angle += get_compass_angle();
	  }
	  compass_angle /= 100;
      // now lookup the angle in the lookup table...
      compass_angle = adjusted(compass_angle);
      flash_angle(compass_angle);
      delayms(2000); // pause before next reading
   }
   return 0;
}

Thanks to David at the Pololu forum for debugging help :smiley: