How to use an I2C chip? (Adafruit MMA8451)

Posted on
  • I've never written stuff for I2C or other protocols, but I have an MMA8451 chip on a breakout board from Adafruit. To my knowledge, there isn't an Espruino library for it, so I've looked at the one provided for Arduino by Adafruit and tried to write one in Espruino, but I'm not getting the expected results and I am rather confused about what I am even doing .. xD

    So quickly a few links:
    This is the Adafruit guide that I used to hook it up
    https://learn.adafruit.com/adafruit-mma8­451-accelerometer-breakout/overview

    This is the datasheet
    https://cdn-shop.adafruit.com/datasheets­/MMA8451Q-1.pdf

    This is the header file for the Adafruit library
    https://github.com/adafruit/Adafruit_MMA­8451_Library/blob/master/Adafruit_MMA845­1.h

    And this is the actual code for the library
    https://github.com/adafruit/Adafruit_MMA­8451_Library/blob/master/Adafruit_MMA845­1.cpp

    Also, I am not proficient in C++, so I've been googling and guessing my way through trying to convert it.

    This is my code, it's in typescript but gets transpiled into JS that the board understands :)

    export default class MMA8451 {
    
      // @ts-ignore
      i2c: I2C = new I2C();
      MMA8451_REG_OUT_X_MSB = 0x01 //!< Read-only device output register
      MMA8451_REG_PL_CFG = 0x11 //!< Portrait/landscape configuration register
      MMA8451_REG_CTRL_REG1 = 0x2A //!< CTRL_REG1 system control 1 register
      MMA8451_REG_CTRL_REG2 = 0x2B //!< CTRL_REG2 system control 2 register
      MMA8451_REG_CTRL_REG4 = 0x2D //!< CTRL_REG4 system control 4 register
      MMA8451_REG_CTRL_REG5 = 0x2E //!< CTRL_REG5 system control 5 register
    
      begin() {
        // @ts-ignore
        this.i2c.setup({ scl: B6, sda: B7 });
        this.i2c.writeTo(this.MMA8451_REG_CTRL_R­EG2, 0x40); // reset
    
        setTimeout(() => {
          this.begin2();
        }, 500);
    
      }
    
      begin2() {
        this.i2c.writeTo(0x0E, 0b01); // enable 4G range
        this.i2c.writeTo(this.MMA8451_REG_CTRL_R­EG2, 0x02); // High res
    
        // DRDY on INT1
        this.i2c.writeTo(this.MMA8451_REG_CTRL_R­EG4, 0x01);
        this.i2c.writeTo(this.MMA8451_REG_CTRL_R­EG5, 0x01);
    
        // Turn on orientation config
        this.i2c.writeTo(this.MMA8451_REG_PL_CFG­, 0x40);
    
        // Activate at max rate, low noise mode
        this.i2c.writeTo(this.MMA8451_REG_CTRL_R­EG1, 0x01 | 0x04);
    
        for (let i = 0x00; i < 0x30; i++) {
                console.log('index', i, this.i2c.readFrom(i, 16));
        }
      }
    
      logAll() {
        for (let i = 0x00; i < 0x30; i++) {
                console.log('index', i, this.i2c.readFrom(i, 16));
        }
      }
    
      logIt() {
        let buffer = new Uint8Array([this.MMA8451_REG_OUT_X_MSB, 0, 0, 0, 0, 0]);
        let val1 = this.i2c.writeTo(buffer, 1);
        let val2 = this.i2c.readFrom(buffer, 6);
    
        let x = buffer[0];
        x <<= 8;
        x |= buffer[1];
        x >>= 2;
        let y = buffer[2];
        y <<= 8;
        y |= buffer[3];
        y >>= 2;
        let z = buffer[4];
        z <<= 8;
        z |= buffer[5];
        z >>= 2;
    
        const divider = 2048;
        let x_g = x / divider;
        let y_g = y / divider;
        let z_g = z / divider;
    
        console.log('x_g', x_g, 'y_g', y_g, 'z_g', z_g);
        console.log('val1', val1);
        console.log('val2', val2);
        console.log('buffer', buffer);
      }
    }
    

    So one thing I am noticing right off the bat is that I never actually use the address of the chip (0x1D), but the Adafruit code also only uses two places, which aren't called in the code xD

    If I run "runSensor" then I get 48 lines printed out that all look like this:
    index 28 new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255])

    Except for one that looks like this:
    index 29 new Uint8Array([0, 41, 252, 163, 200, 152, 160, 0, 41, 252, 163, 200, 152, 160, 0, 41])

    But that line doesn't change no matter how much I move the sensor around
    And if I run "logIt"
    I get the following data:
    x_g 0.03125 y_g 0 z_g 0
    val1 undefined
    val2 new Uint8Array([255, 255, 255, 255, 255, 255])
    buffer new Uint8Array([1, 0, 0, 0, 0, 0])

    Again it's identical no matter how much I move the sensor around.
    If I unplug the sensor, then line 29 in the array becomes an array of 255 like the others, however, the "logIt" still pumps out the same data.

    Can anyone help me with this? :)

  • I would advise you to review the Espruino documentation on the I2C.writeTo function:

    Call type:
    function I2C.writeTo(address, data, ...)
    Parameters
    
    address - The 7 bit address of the device to transmit to, or an object of the form {address:12, stop:false} to send this data without a STOP signal.
    
    data, ... - One or more items to write. May be ints, strings, arrays, or special objects (see E.toUint8Array for more info).
    

    and also what the Adafruit writeRegister8 function does, namely pack the two arguments together and send them both to the device.

    void Adafruit_MMA8451::writeRegister8(uint8_t­ reg, uint8_t value) {
      uint8_t buffer[2] = {reg, value};
      i2c_dev->write(buffer, 2);
    }
    

    In short, your existing code appears to read and write from a variety of random device addresses, none of which have any corresponding I2C bus device apart from 29(0x1D).

  • Thank you for your reply :)
    After reading and thinking I tried rewriting all of my read and write calls, though I am still quite confused over exactly what the "writeRegister" function actually does.

    What I am guessing so far, is that the _sensorID is set via "getSensor", which is called at some point, though as far as I can see it's not called in the code nor in the examples from Adafruit.
    From then on it's automagically inserted in the write function called by writeRegister8?

    I am assuming that

    uint8_t buffer[2] = {reg, value}
    

    creates a new "Uint8Array" of length 2, and adds the reg on position 0 and the value on position 1?

    Then from what I can read

    i2c_dev.write(buffer,2)
    

    in js would be akin to

    this.i2c.writeTo(0x1D, [reg, val])
    

    Is that correct? Not certain what to do with the 2 though.

    I tried changing my read/write calls to look like the following

    this.i2c.writeTo(this.MMA8451_ADDR, [this.MMA8451_REG_CTRL_REG2, 0x02]);
    

    I am still quite confused about how to do this then:

    i2c_dev->write_then_read(buffer, 1, buffer, 6);

    I attempted to write it like this

    let buffer = new Uint8Array([this.MMA8451_REG_OUT_X_MSB, 0, 0, 0, 0, 0]);
        this.i2c.writeTo(this.MMA8451_ADDR, [buffer, 1]);
        let val2 = this.i2c.readFrom(this.MMA8451_ADDR, 6);
    

    But I'm pretty sure that's wrong, I am getting changing values at least, but they seem random xD
    I just had the chip laying on my desk and ran "logIt" a few times in row, and got these values.

    x_g 7.97021484375 y_g 6.5048828125 z_g 7.001953125
    val2 new Uint8Array([255, 15, 208, 41, 224, 17])

    x_g 7.97607421875 y_g 4.25146484375 z_g 2
    val2 new Uint8Array([255, 61, 136, 12, 64, 3])

    x_g 7.97607421875 y_g 7.7509765625 z_g 6
    val2 new Uint8Array([255, 60, 248, 11, 192, 3])

    x_g 7.97607421875 y_g 4.0009765625 z_g 3.5
    val2 new Uint8Array([255, 63, 128, 11, 112, 3])

    x_g 7.97607421875 y_g 4.7509765625 z_g 3.00048828125
    val2 new Uint8Array([255, 62, 152, 11, 96, 4])

    x_g 7.97607421875 y_g 1.00146484375 z_g 4.5
    val2 new Uint8Array([255, 63, 32, 12, 144, 3])

    x_g 7.97607421875 y_g 5.5009765625 z_g 4.75
    val2 new Uint8Array([255, 62, 176, 9, 152, 3])

    x_g 7.97607421875 y_g 6.2509765625 z_g 0.5
    val2 new Uint8Array([255, 63, 200, 11, 16, 3])

    x_g 7.9765625 y_g 6.7509765625 z_g 0
    val2 new Uint8Array([255, 64, 216, 11, 0, 3])

    x_g 7.97607421875 y_g 7.2509765625 z_g 7.00048828125
    val2 new Uint8Array([255, 61, 232, 8, 224, 4])

    x_g 7.97607421875 y_g 4.5009765625 z_g 7.00048828125
    val2 new Uint8Array([255, 62, 144, 9, 224, 4])

    x_g 7.97607421875 y_g 0.5009765625 z_g 1.75048828125
    val2 new Uint8Array([255, 63, 16, 9, 56, 4])

    x_g 7.97607421875 y_g 5.00048828125 z_g 6.75048828125
    val2 new Uint8Array([255, 62, 160, 7, 216, 4])

    x_g 7.97607421875 y_g 0.0009765625 z_g 2.50048828125
    val2 new Uint8Array([255, 63, 0, 8, 80, 5])

    x_g 7.97607421875 y_g 3.5009765625 z_g 0.75048828125
    val2 new Uint8Array([255, 63, 112, 9, 24, 4])

    As you can see, despite laying on my desk, X is 7.976 all throughout while Y and Z are fluctuating, so I'm pretty sure I'm still doing something wrong, but I feel as though I'm closer at least :)

  • It's rather confusing, but as I understand it a 'device', and the i2c object in Arduino represents a single sensor(or whatever) that is connected to the i2c bus . Meanwhile, the 'device'/i2c object in Espruino is the i2c port on the Espruino board(it might have several). This means all calls to read and write on Arduino are automatically prefixed with the address, wheras on Espruino you have to supply the address in every function call.

    I would advise you to look at the function documentation and try to understand what is being done, instead of trying to translate every function argument from language 1 to language 2.

    If you review the function arguments of write_then_read , it can be seen that a call of write_then_read(buffer, 1, buffer, 6) sends the first byte of buffer to the device, then reads 6 bytes into the same memory. Perhaps you are not aware of this, but in C/C++, arrays do not know their own size(an 'array' variable is actually just the memory address of the first entry) so the size must always be supplied separately.

    Your code sends the entire buffer plus a byte of value of '1' to the device, then reads 6 bytes into 'val2'.

  • Perhaps you are not aware of this, but in C/C++, arrays do not know their own size(an 'array' variable is actually just the memory address of the first entry) so the size must always be supplied separately.

    I had no idea no. I've done C# and js/ts, but not much C or C++.

    What you write makes me think that I should do something like this

        this.i2c.writeTo(this.MMA8451_ADDR, this.MMA8451_REG_OUT_X_MSB);
        let buffer = this.i2c.readFrom(this.MMA8451_ADDR, 6);
    

    Where I then only send the value the "write_then_read" function sends, and then read 6 bytes from the same address, however the data I get out still seems wrong, here are 4 readings and again it's just lying on the table not moving.

    x_g 7.9755859375 y_g 2.25146484375 z_g 5.50146484375
    buffer new Uint8Array([255, 59, 72, 13, 176, 12])
    
    x_g 7.9755859375 y_g 0.50146484375 z_g 4.75146484375
    buffer new Uint8Array([255, 59, 16, 13, 152, 12])
    
    x_g 7.9755859375 y_g 4.75146484375 z_g 3.75146484375
    buffer new Uint8Array([255, 59, 152, 14, 120, 12])
    
    x_g 7.9755859375 y_g 1.00146484375 z_g 6.25146484375
    buffer new Uint8Array([255, 59, 32, 13, 200, 12])
    
  • I have rewritten my code and cleaned it up a bit, but it still gives the same values as in my previous post

    export default class MMA8451 {
    
      // @ts-ignore
      i2c: I2C = new I2C();
      MMA8451_ADDR = 0x1D;
      MMA8451_REG_OUT_X_MSB = 0x01 //!< Read-only device output register
      MMA8451_REG_PL_CFG = 0x11 //!< Portrait/landscape configuration register
      MMA8451_REG_CTRL_REG1 = 0x2A //!< CTRL_REG1 system control 1 register
      MMA8451_REG_CTRL_REG2 = 0x2B //!< CTRL_REG2 system control 2 register
      MMA8451_REG_CTRL_REG4 = 0x2D //!< CTRL_REG4 system control 4 register
      MMA8451_REG_CTRL_REG5 = 0x2E //!< CTRL_REG5 system control 5 register
    
      writeRegister8(reg: number, value: number) {
        this.i2c.writeTo(this.MMA8451_ADDR, [reg, value]);
      }
    
      readRegister8(reg: number) {
        this.i2c.writeTo(this.MMA8451_ADDR, reg);
        return this.i2c.readFrom(this.MMA8451_ADDR, 1)[0];
      }
    
    
      begin() {
        // @ts-ignore
        this.i2c.setup({ scl: B6, sda: B7 });
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG2, 0x40)// reset
    
        setTimeout(() => {
          this.begin2();
        }, 500);
      }
    
      begin2() {
        this.writeRegister8(0x0E, 0b01); // enable 4G range
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG2, 0x02); // High res
    
        // DRDY on INT1
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG4, 0x01);
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG5, 0x01);
    
        // Turn on orientation config
        this.writeRegister8(this.MMA8451_REG_PL_­CFG, 0x40);
    
        // Activate at max rate, low noise mode
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG1, 0x01 | 0x04);
      }
    
      logIt() {
        this.i2c.writeTo(this.MMA8451_ADDR, this.MMA8451_REG_OUT_X_MSB);
        let buffer = this.i2c.readFrom(this.MMA8451_ADDR, 6);
    
        let x = buffer[0];
        x <<= 8;
        x |= buffer[1];
        x >>= 2;
        let y = buffer[2];
        y <<= 8;
        y |= buffer[3];
        y >>= 2;
        let z = buffer[4];
        z <<= 8;
        z |= buffer[5];
        z >>= 2;
    
        const divider = 2048;
        let x_g = x / divider;
        let y_g = y / divider;
        let z_g = z / divider;
    
        console.log('x_g', x_g, 'y_g', y_g, 'z_g', z_g);
        console.log('buffer', buffer);
      }
    }
    
  • I've noticed something.
    Other implementations of read take a registry variable, and then write to that registry first and then read the first byte from the device, but after a lot of testing it seems that the first byte here is always either 0 or 255
    The device sets that byte from 0 to 255 when this is called:

        // Activate at max rate, low noise mode
        this.writeRegister8(this.MMA8451_REG_CTR­L_REG1, 0x01 | 0x04);
    

    After work I'll go back and look at the datasheet, but I am wondering if perhaps it just always sends the data in the array back in the same order, no matter what is sent to it.

  • Lol ... so working on that theory, I changed the logIt function to look like this

    logIt() {
        this.i2c.writeTo(this.MMA8451_ADDR, this.MMA8451_REG_OUT_X_MSB);
        let buffer = this.i2c.readFrom(this.MMA8451_ADDR, 7);
    
        let x = buffer[1];
        x <<= 8;
        x |= buffer[2];
        x >>= 2;
        let y = buffer[3];
        y <<= 8;
        y |= buffer[4];
        y >>= 2;
        let z = buffer[5];
        z <<= 8;
        z |= buffer[6];
        z >>= 2;
    
        const divider = 2048;
        let x_g = x / divider;
        let y_g = y / divider;
        let z_g = z / divider;
    
        console.log('x_g', x_g, 'y_g', y_g, 'z_g', z_g);
        console.log('buffer', buffer);
      }
    

    Essentially I'm just ignoring the first byte of data and grabbing 1 extra, and now the data at least seems relatively stable, though I think it's the tilt of the sensor, but for some reason in a range between 0 and 8 o_0, but it really doesn't fit with how I've understood I2C to work.

    From what I understand you send a write, telling what you want from the device, when you call read the first data you get back is the data you requested. I am rather confused ... xD

  • The write...read pattern seems right. If you check out the libraries for some of the existing modules you'll see it's done in that way (http://www.espruino.com/I2C#using-i2c then you can usually go to a page a find a link to the module there). For instance in http://www.espruino.com/modules/VL53L0X.­js there are two functions r and w:

    VL53L0X.prototype.r = function(addr,n) {
      this.i2c.writeTo(this.ad, addr);
      return this.i2c.readFrom(this.ad, n);
    };
    VL53L0X.prototype.w = function(addr,d) {
      this.i2c.writeTo(this.ad, addr, d);
    };
    

    What you might be missing is some chips only read properly if you do an I2C write and read without something called a "stop" between. You can force that my changing the address var to an object as in: http://www.espruino.com/Reference#l_I2C_­writeTo

    In your case maybe:

        this.i2c.writeTo({address:this.MMA8451_A­DDR, stop:false}, this.MMA8451_REG_OUT_X_MSB);
        let buffer = this.i2c.readFrom(this.MMA8451_ADDR, 7);
    
  • Ah, thank you, I am still not getting the accelleration (.. I think), but I just had a breakthrough, by setting stop:false, I just ran the following piece of code.

    I2C1.writeTo({ address: 0x1D, stop: false }, 0x0D);I2C1.readFrom(0x1D, 1);
    

    What this is supposed to do is return the device's ID, 0x1A, and I got 26 back, which indeed is 0x1A, I feel like I am really close to cracking this, thank you :D

  • Ok, I can get a few things consistently from the sensor including the orientation, but I found this section:

    Many applications use the accelerometer’s static acceleration readings (i.e., tilt) which measure the change in acceleration
    due to gravity only. These functions benefit from acceleration data being filtered with a low-pass filter where high frequency data is
    considered noise.

    In the documentation, I wonder if I'm getting "static acceleration" data rather than actual acceleration data, I'll have to dig more tomorrow, my brain at this point is mushy .. xD
    Thanks for your help both of you :)

  • In the documentation, I wonder if I'm getting "static acceleration" data rather than actual acceleration data

    It could be... It might help to graph the data somehow when it's moving to see if it makes sense.

    Normally from an accelerometer you'd expect to have the value including gravity, so when you lie it flat on a surface one axis should be 1g and the others 0g (ish).

    Looks like you're not doing any checks for the sign of the value either, so it'd likely negative values will just show up as large positive numbers. It might be worth looking at some of Espruino's other accelerometer modules to see how that gets handled usually.

  • I am not sure how I would go about graphing it, I am running it on a pico via VS Code.
    However, I tried printing out a continuous stream of x,y and z values, and I can see a bump in the correct direction when I move the sensor around.

    And as you wrote it is showing one value as 1 and the other 2 as zero, so it seems to be the acceleration + gravity.
    I found a few implementations of the code in JS but for raspberry pi etc, and I can't see how they handle filtering out the gravity, but I'll keep on digging, I'll also look at how Espruino handles other accelerometers, thank you :)

  • Just want to post that I finally got something that works, while filtering out the gravity, I am not sure what I did, but I'll clean up my code tomorrow and figure it out xD

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

How to use an I2C chip? (Adafruit MMA8451)

Posted by Avatar for TheLogan @TheLogan

Actions