Sunday, March 11, 2012

MMA7660FC Accelerometer on dual voltage I2C bus

I recently got a Freescale MMA7660FC accelerometer already soldered on a breakout board. It's made by made by Tautic and comes complete with decoupling capacitors. I soldered a header to the board and plugged in a 5-pin connector. The connector wires plug into the breadboard. This way I can freely rotate the accelerometer.

To make sure the communications between the microcontroller and the accelerometer was going well and to view the values of X, Y, Z axes, I initially relied on the Saleae Logic. After the bugs had been ferreted out I wired in an LCD so I can get a real time feedback as I rotated the board.

Note that Tautic has opted to tie the analog (AVDD) and digital (DVDD) supply together. This means the chip cannot be set to OFF MODE wherein only the digital part of the circuit is powered. According to the datasheet in this mode the "MMA7660FC will not respond, but I2C bus can be used for other devices (MMA7660FC does not load I2C bus)." This mode has the least power consumption and would be useful in handheld devices which need to conserve as much power as possible.

While I'm quoting the datasheet, a caveat is in order. To have absolute faith in the 7660 datasheet is to court misery--as I learned. Even as it's already in its 7th revision it still has at least two egregious errors which caused me confusion, not to mention wasted time in getting this chip up and running.

The first brick wall I crashed into: "During the Active Mode, continuous measurement on all three axes is enabled. In addition, the user can choose to enable: Shake Detection, Tap Detection, Orientation Detection, and/or Auto-Wake/Sleep Feature..." Well I tried to change a few registers in Active Mode and it wouldn't let me. So I did it in Standby Mode. I'm not sure if none of the registers--save for MODE register 0x07--can be edited in Active Mode.

Second Freescale booboo: "In order to enable Tap detection in the device the user must enable the Tap Interrupt in the INTSU (0x06) register...." No! I disabled all interrupts and tap detection still works. Of course there won't be any interrupts sent via INT. But the tap detection bit in TILT register 0x03 still gets set when the chip is tapped.

Can Freescale engineers please get their documentation right? This isn't a 300-page doc. It shouldn't be that hard to finally--after so many revisions--get rid of all the errors.

The MMA7660 has a maximum VDD of 3.6V and a recommended VDD of 2.8V. Maximum voltage on any other pin should not exceed VDD. Since it communicates with the microcontroller via I2C the MCU must either run at the same voltage or if the MCU runs at a higher voltage then voltage level shifting hardware has to be present on the I2C bus.

The microcontroller I chose is the 20-pin PIC16F1828. Because I'm using a 5V Displaytech 162F LCD I wanted the MCU to run at 5V. This meant I had to find a way to have a dual voltage I2C bus. Fortunately, the engineers at Philips devised a very simple way to realize this years ago. Check out the following application notes:

AN97055 Bi-directional level shifter for I²C-bus and other systems 
AN10441 Level shifting techniques in I2C-bus design

We need only connect two MOSFETS to the bus and we automatically have a bidirectional voltage translator, with electrical isolation between the two voltage levels. In the breadboarded circuit I had an LM317 voltage regulator supply 2.8V to the 7660 while the MCU and LCD were run at 5.2V. The MOSFETs were 2N7000.
The comparator isolates the INT pin from the MCU. It also provides either 0V or 5V signal to the MCU. Note that the accelerometer's INT pin is configurable either as open drain or push-pull. In this set up it should be set as push-pull. In the test circuit I didn't use a comparator. Instead I employed a simple diode to isolate and protect the accelerometer. The pull down resistor prevents the MCU input pin from floating when INT is low. The voltage output at the INT pin is actually above the minimum of 2.0V required by the TTL input of the MCU. Note that given an INT max output of 2.8V this diode method will not work with Schmitt trigger inputs on the microcontroller since they require a logic high to be at least 80% of VDD.

For this test circuit I've added a 3.3V Zener diode and a 27-ohm current limiting resistor to protect the accelerometer from any accidental overvoltages.

Here are screenshots of samples taken by the Saleae Logic analyzer on both the 5V and 2.8V I2C bus. You'll  notice that the sampling rate is a mere 500kHz. This is a limitation of my computer. I don't have a USB 2.0 connection and therefor--according Logic--condemns me to this max rate. Else I'd definitely be sampling at >1MHz.





The following vid shows how the accelerator's measured values change as it's tapped, turned on its sides, and flipped over. The XYZ values shown on the LCD are in the range of -31 to +31. They are not percentages of g.




I've already tested the tap interrupt function but have yet to try the other interrupts as well as shake detection and auto-sleep / auto-wake functions.

In the firmware I rely on mikroC's built-in functions for accessing the I2C bus and the LCD. The splash screen routine wasn't installed in the firmware I used in the vid.

/*

Displaying the measurements of the Freescale MMA7660FC Accelerometer on an LCD

Created:        March 2012
Processor:      PIC 16F1828
Compiler        mikroC Pro 5.0.0
Configuration:  power up timer, brownout reset (set to 2.5V), stack over/underflow reset -- all enabled, all others disabled
                internal oscillator with clk pins as I/O

*/


#define  t1h_ini   256 - 122           // value loaded into TMR1H every time it rolls over to zero for timer1 to count 0.25sec
                                       // at a prescale = 1:8 and clock of 4MHz

#define  I2Cclock 100000               // mikroC needs to know the desired I2C clock frequency (in Hz) in order to compute baud rate generator values for PIC registers

// MMA7660FC 7-bit address = 0x4C, left shift by one bit to become 0x98 in 8-bit format. 
// LSB = 0 to read slave, 1 to write to slave
#define   ACCwr    0x98 + 0
#define   ACCrd    0x98 + 1


int8 XOUT, YOUT, ZOUT;                 // X,Y,Z axis values from accelerometer - register addresses 0x00, 0x01, 0x02 respectively
                                       // values range from -31 to +32, with negative values in 2's complement
                                       // if bit 5 = 1 then number is negative. To retrieve the value, get the complement and then add 1
                                       // if bit 6 = 1 then value is not accurate because accelerometer was writing to register when it was read

int8  TILT;                            // contains the value of register address 0x03 of accelerometer

bit tap;                               // flag for tap detect


// following are needed by mikroC for built-in LCD functions

// Lcd pinout settings
sbit LCD_RS at RC1_bit;
sbit LCD_EN at RA2_bit;
sbit LCD_D7 at RC4_bit;
sbit LCD_D6 at RC3_bit;
sbit LCD_D5 at RC6_bit;
sbit LCD_D4 at RC7_bit;

// Pin direction
sbit LCD_RS_Direction at TRISC1_bit;
sbit LCD_EN_Direction at TRISA2_bit;
sbit LCD_D7_Direction at TRISC4_bit;
sbit LCD_D6_Direction at TRISC3_bit;
sbit LCD_D5_Direction at TRISC6_bit;
sbit LCD_D4_Direction at TRISC7_bit;


// **************************************************************************
/*
   for LCD with Hitachi HD44780 (or compatible) controller chip
   c1 = is the address of line 1 column 1 of the LCD
   l2c1 = line 2 column 1
*/
// **************************************************************************

#define  c1                  0x00 + 0x80
#define  c2                  0x01 + 0x80
#define  c3                  0x02 + 0x80
#define  c4                  0x03 + 0x80
#define  c5                  0x04 + 0x80
#define  c6                  0x05 + 0x80
#define  c7                  0x06 + 0x80
#define  c8                  0x07 + 0x80
#define  c9                  0x08 + 0x80
#define  c10                 0x09 + 0x80
#define  c11                 0x0a + 0x80
#define  c12                 0x0b + 0x80
#define  c13                 0x0c + 0x80
#define  c14                 0x0d + 0x80
#define  c15                 0x0e + 0x80
#define  c16                 0x0f + 0x80
#define  c17                 0x10 + 0x80
#define  c18                 0x11 + 0x80
#define  c19                 0x12 + 0x80
#define  c20                 0x13 + 0x80
#define  c21                 0x14 + 0x80
#define  c22                 0x15 + 0x80
#define  c23                 0x16 + 0x80
#define  c24                 0x17 + 0x80
#define  c25                 0x18 + 0x80
#define  c26                 0x19 + 0x80
#define  c27                 0x1a + 0x80
#define  c28                 0x1b + 0x80
#define  c29                 0x1c + 0x80
#define  c30                 0x1d + 0x80
#define  c31                 0x1e + 0x80
#define  c32                 0x1f + 0x80
#define  c33                 0x20 + 0x80
#define  c34                 0x21 + 0x80
#define  c35                 0x22 + 0x80
#define  c36                 0x23 + 0x80
#define  c37                 0x24 + 0x80
#define  c38                 0x25 + 0x80
#define  c39                 0x26 + 0x80
#define  c40                 0x27 + 0x80

#define  l2c1                0x40 + 0x80
#define  l2c2                0x41 + 0x80
#define  l2c3                0x42 + 0x80
#define  l2c4                0x43 + 0x80
#define  l2c5                0x44 + 0x80
#define  l2c6                0x45 + 0x80
#define  l2c7                0x46 + 0x80
#define  l2c8                0x47 + 0x80
#define  l2c9                0x48 + 0x80
#define  l2c10               0x49 + 0x80
#define  l2c11               0x4a + 0x80
#define  l2c12               0x4b + 0x80
#define  l2c13               0x4c + 0x80
#define  l2c14               0x4d + 0x80
#define  l2c15               0x4e + 0x80
#define  l2c16               0x4f + 0x80
#define  l2c17               0x50 + 0x80
#define  l2c18               0x51 + 0x80
#define  l2c19               0x52 + 0x80
#define  l2c20               0x53 + 0x80
#define  l2c21               0x54 + 0x80
#define  l2c22               0x55 + 0x80
#define  l2c23               0x56 + 0x80
#define  l2c24               0x57 + 0x80
#define  l2c25               0x58 + 0x80
#define  l2c26               0x59 + 0x80
#define  l2c27               0x5a + 0x80
#define  l2c28               0x5b + 0x80
#define  l2c29               0x5c + 0x80
#define  l2c30               0x5d + 0x80
#define  l2c31               0x5e + 0x80
#define  l2c32               0x5f + 0x80
#define  l2c33               0x60 + 0x80
#define  l2c34               0x61 + 0x80
#define  l2c35               0x62 + 0x80
#define  l2c36               0x63 + 0x80
#define  l2c37               0x64 + 0x80
#define  l2c38               0x65 + 0x80
#define  l2c39               0x66 + 0x80
#define  l2c40               0x67 + 0x80


#define  int1                bit
#define  int8                unsigned char
#define  int16               unsigned int
#define  int32               unsigned long

#define  on                  1
#define  off                 0

#define  input               1         // for TRISx
#define  output              0         // for TRISx

#define  analog              1         // for ANSELx
#define  digital             0         // for ANSELx

// ************************************************************************************************
//       I2C
// ************************************************************************************************

// initialize the accelerometer
void ACCini()
{
  // upon power up the MMA7660 defaults to Standby Mode
  // apparently the MMA7660 registers can only be changed when it's in Standby Mode
  I2C1_Init(I2Cclock);       // mikroC needs to set up PIC I2C registers given the user desired I2C frequency
  I2C1_Start();
  I2C1_Wr(ACCwr);            // transmit MMA7660 address and write command
  I2C1_Wr(0x6);              // point to register address 0x06
  I2C1_Wr(0b0);              // [reg addr 0x06] all interrupts disabled
  I2C1_Wr(0b0);              // [reg addr 0x07]
  I2C1_Wr(0b0);              // [reg addr 0x08] set to 120 samples/sec -- required for tap detect
  I2C1_Wr(0b01101111);       // [reg addr 0x09] set
  I2C1_Wr(0x0F);             // [reg addr 0x0A]
  I2C1_Stop();

  // after registers have been initialized it can be put in Active Mode where it can start measuring values
  I2C1_Start();
  I2C1_Wr(ACCwr);            // issue MMA7660 address and write command
  I2C1_Wr(0x7);              // point to register address 0x06
  I2C1_Wr(0b11000001);       // [reg addr 0x07] put MMA7660 in Active Mode. Upon power up chip enters Standby Mode
                             // interrupt is push-pull, interrupt is active high
  I2C1_Stop();
} // void ACCini()


// VAL contains the value of accelerometer value of the axes
void DispAxis(int8 row, int8 col, int8 VAL)
{
  #define          pos       1
  #define          neg       0
  bit sign;                  // 1 = number is positive; 0 = number is negative

  sign = pos;
  if (VAL.f5)                // if bit5 is high then axis value is negative -- in two's complement
  {
    VAL.f6 = 1;              // this bit is not part of the axis value so make it high so that when it's compelemented it'll be zero
    VAL.f7 = 1;              // this bit is not part of the axis value so make it high so that when it's compelemented it'll be zero
    VAL = ~VAL + 1;          // get the absolute value from the twos complement
    sign = neg;
  }

  if (sign == pos)
    Lcd_Chr(row,col, '+');
  else
    Lcd_Chr(row,col, '-');

  // display the tens and ones digit of the axis value
  Lcd_Chr_CP(VAL/10 + 0x30); // the ASCII code of a number is the number + 0x30
  Lcd_Chr_CP(VAL%10 + 0x30); // the ASCII code of a number is the number + 0x30
}


// read and display measurements from the accelerometer
void ACCread()
{
  int8 VAL;
  
  do
  {
    I2C1_Start();
    I2C1_Wr(ACCwr);            // issue MMA7660 adddress and write command
    I2C1_Wr(0x0);              // point to desired register adddress
    I2C1_Repeated_Start();
    I2C1_Wr(ACCrd);            // issue MMA7660 adddress and read command
    XOUT = I2C1_Rd(1);
    YOUT = I2C1_Rd(1);
    ZOUT = I2C1_Rd(1);
    TILT = I2C1_Rd(0);
    I2C1_Stop();
  } while (XOUT.f6 || YOUT.f6 || ZOUT.f6 || TILT.f6);     // if bit6 of these registers is high then the registers were read while the chip was updating them
                                                          // read the registers again since the values will be unreliable

  // X Y Z axes output
  DispAxis(1, 3, XOUT);
  DispAxis(1, 9, YOUT);
  DispAxis(1, 14, ZOUT);

  // Back / Front orientation
  VAL = TILT & 0b11;
  Lcd_Cmd(l2c1);
  if (!VAL)
    Lcd_Out_CP("  ?  ");
  else if (VAL == 1)
    Lcd_Out_CP("FRONT");
  else if (VAL == 2)
    Lcd_Out_CP("BACK ");
  else
    Lcd_Out_CP("error");

  // Portrait / Landscape orientation
  Lcd_Cmd(l2c7);
  VAL = (TILT & 0b11100) >> 2;
  if (!VAL)
    Lcd_Out_CP("  ?  ");
  else if (VAL == 1)
    Lcd_Out_CP("LEFT ");
  else if (VAL == 2)
    Lcd_Out_CP("RIGHT");
  else if (VAL == 5)
    Lcd_Out_CP("DOWN ");
  else if (VAL == 6)
    Lcd_Out_CP("UP   ");
  else
    Lcd_Out_CP("error");

  // check for tap detect
  // single tap = (a tap is detected in the current I2C read) && (no tap was detected during the last I2C read || a double tap was detected during the previous read )
  // double tap = a tap is detected in the current read && a single tap was detected in the last I2C read 
  Lcd_Cmd(l2c13);
  if (TILT.f5)
  {
    if (!tap)
    {
      tap = 1;
      Lcd_Chr_CP('1');
    }
    else
    {
      tap = 0;
      Lcd_Chr_CP('2');
    }
  }
  else
  {
    Lcd_Chr_CP('-');
    tap = 0;
  }
} // void ACCread()


// Splash screen and permanent text on LCD
void DispIniText()
{
  Lcd_Cmd(_LCD_CLEAR);               // Clear display
  Lcd_Cmd(_LCD_CURSOR_OFF);          // Cursor off

  // splash screen upon power up
  Lcd_Out(1, 1, "FreescaleMMA7660");
  Lcd_Out(2, 2, "Accelerometer");
  Delay_ms(3000);
  Lcd_Cmd(_LCD_CLEAR);               // Clear display

  // Text that will always remain on screen
  Lcd_Out(1, 1, "X=");
  Lcd_Out(1, 7, "Y=");
  Lcd_Chr(1, 13, 'Z');
  Lcd_Out(2, 14, "TAP");
}


// ************************************************************************************************
//       other functions
// ************************************************************************************************

void IniReg()
{
  // set internal clock frequency to 4MHz
  OSCCON = 0b1101000;

  TRISA = output;
  TRISB = output;
  TRISC = output;
  ANSELA = digital;
  ANSELB = digital;
  ANSELC = digital;
  PORTA = 0;
  PORTB = 0;
  PORTC = 0;

  TRISB.f7 = input;
  IOCBP.IOCBP7 = 1;
  IOCBF = 0;

  // Timer 1
  TMR1L = 0;
  TMR1H = t1h_ini;
  PIR1.TMR1IF = 0;
  T1CON = 0b110001;          // prescale = 1:8, timer1 clock = Fosc/4, timer1 oscillator off, timer1 on

  // Timer2 is used for state machine and switch debouncing timing tick
  // with clock = 2MHz, PR2 = 125, prescale = 1:16, postscale = 1:1
  // TMR2 will count from zero to PR2 and timer2 interrupt occurs every 125*16 / (2MHz / 4) = 4ms = timer2 tick
//  T2CON = 0b110;             // postscaler = 1:1, prescaler = 1:16, timer2 on
//  TMR2 = 0;
//  PR2 = 125;

  WDTCON = 0b1000;           // prescale = 1:512 (16ms typical)
                             // WDTE in configuration word is configured so that WDT enabled when MCU awake and disabled when MCU asleep

  tap = 0;
} // void IniReg()


// ************************************************************************************************
//       main
// ************************************************************************************************

void main()
{
  IniReg();
  Lcd_Init();
  DispIniText();
  ACCini();
  while(1)
  {
    if (PIR1.TMR1IF)
    {
      TMR1H = t1h_ini;
      ACCread();
      PIR1.TMR1IF = 0;
    }
  }
} // void main()

1 comment:

  1. Just discovered this piece of code.
    I dont understand that the I2C1_Wr(0x0); will only write to the 0 register. How do the 01,02,03 registers get read
    do
    {
    I2C1_Start();
    I2C1_Wr(ACCwr); // issue MMA7660 adddress and write command
    I2C1_Wr(0x0); // point to desired register adddress
    I2C1_Repeated_Start();
    I2C1_Wr(ACCrd); // issue MMA7660 adddress and read command
    XOUT = I2C1_Rd(1);
    YOUT = I2C1_Rd(1);
    ZOUT = I2C1_Rd(1);
    TILT = I2C1_Rd(0);
    I2C1_Stop();
    } while (XOUT.f6 || YOUT.f6 || ZOUT.f6 || TILT.f6);

    ReplyDelete