MPL3115A2 Altitude Sensor One Shot Mode

The good people at hobbytronics sell a neat altimeter based on the MPL3115A2 pressure sensor. Its datasheet mentions a one shot mode, which can be a practical solution if one needs a high data acquisition frequency. For some reasons, I did not get the FIFO polling mode to produce new readings at intervals shorter than 512ms. Maybe someone can enlighten me. The main benefit of using the one shot mode is not having to constantly poll the interrupt register to see whether new data has arrived (if you don’t want to connect an additional wire to one of the interrupts pin). As soon as the one shot bit is cleared and reset, a new measurement cycle begins. Its duration is determined by the oversampling bits. The trick is to match loop time with this measurement cycle. For example, if you want to run the loop every 500ms, set oversampling to 128x and you will get roughly one new value every time the registers are read. I clear the one shot bit immediately after reading the register to start a new measurement cycle, so we can do other things while the sensor is busy. The other improvement is reading all registers at once instead of querying them individually, which saves a few bytes of I2C communication.

In general, filtering the data yourself (for example, reading at shorter intervals, but with less oversampling) does not increase precision, but may be more timely depending on the filter you use. Unfortunately, the datasheet does not say how exactly individual readings are combined by the sensor, but it looks like simple averaging (which, of course, is the best linear estimator of the mean).

In case you wonder what the lengthy setup() function does: The aim is to calibrate the altitude readings using the actual sea level pressure, which can only be inferred from a current pressure reading at a known altitude. Hence, you have to provide an altitude (e.g. by hard-coding the altitude or via GPS). The algorithm calculates the sea level pressure using the formula in the datasheet. Theoretically, feeding the sea level pressure back to the sensor, that is, writing to the barometer offset register, should yield an altimeter reading of the known altitude we just used to calculate the pressure. It does not, which implies that the sensor is not (only) using the formula provided in the datasheet. The resulting altitude offset is then saved in a variable to be used when printing the altitude. You could also write this offset to the sensor register to save one addition, but this is less precise (integers only), and you would have to set it back to zero before calculating the sea level pressure at start-up. [The offset was caused by a bug in the pressure calculation code. Thanks to Mike for pointing this out. The altitude setup now works without any offset.]

There also is some background information available from Freescale on how the sensor works in principle and how various offsets can be accounted for.

This is my working example for Arduino:

/*

MPL3115A2 Altitude Sensor One Shot Mode Example
 Henry Lahr, 2013-02-27
 loosely based on: https://github.com/sparkfun/MPL3115A2_Breakout/blob/master/firmware/mpl3115a2/mpl3115a2.ino
 There is no warranty for this code; feel free to use this code for your projects.
 Hardware Connections: VCC = 3.3V; SDA = A4; SCL = A5; INT pins not connected
 Usage:
 - Serial terminal at 115200bps
 - Prints altitude in meters or temperature in degrees C, depending on whether ALTMODE is defined
 */

#include <Wire.h> // for I2C communication

#define ALTMODE; //comment out for barometer mode; default is altitude mode
#define ALTBASIS 18 //start altitude to calculate mean sea level pressure in meters
//this altitude must be known (or provided by GPS etc.)

const int SENSORADDRESS = 0x60; // address specific to the MPL3115A1, value found in datasheet

float altsmooth = 0; //for exponential smoothing
byte IICdata[5] = {0,0,0,0,0}; //buffer for sensor data

void setup(){
  Wire.begin(); // join i2c bus
  Serial.begin(115200); // start serial for output
  Serial.println("Setup");
  if(IIC_Read(0x0C) == 196); //checks whether sensor is readable (who_am_i bit)
  else Serial.println("i2c bad");

  IIC_Write(0x2D,0); //write altitude offset=0 (because calculation below is based on offset=0)
  //calculate sea level pressure by averaging a few readings
  Serial.println("Pressure calibration...");
  float buff[4];
  for (byte i=0;i<4;i++){
    IIC_Write(0x26, 0b00111011); //bit 2 is one shot mode, bits 4-6 are 128x oversampling
    IIC_Write(0x26, 0b00111001); //must clear oversampling (OST) bit, otherwise update will be once per second
    delay(550); //wait for sensor to read pressure (512ms in datasheet)
    IIC_ReadData(); //read sensor data
    buff[i] = Baro_Read(); //read pressure
    Serial.println(buff[i]);
  }
  float currpress=(buff[0]+buff[1]+buff[2]+buff[3])/4; //average over two seconds

  Serial.print("Current pressure: "); Serial.print(currpress); Serial.println(" Pa");
  //calculate pressure at mean sea level based on a given altitude
  float seapress = currpress/pow(1-ALTBASIS*0.0000225577,5.255877);
  Serial.print("Sea level pressure: "); Serial.print(seapress); Serial.println(" Pa");
  Serial.print("Temperature: ");
  Serial.print(IICdata[3]+(float)(IICdata[4]>>4)/16); Serial.println(" C");

  // This configuration option calibrates the sensor according to
  // the sea level pressure for the measurement location (2 Pa per LSB)
  IIC_Write(0x14, (unsigned int)(seapress / 2)>>8);//IIC_Write(0x14, 0xC3); // BAR_IN_MSB (register 0x14):
  IIC_Write(0x15, (unsigned int)(seapress / 2)&0xFF);//IIC_Write(0x15, 0xF3); // BAR_IN_LSB (register 0x15):

  //one reading seems to take 4ms (datasheet p.33);
  //oversampling 32x=130ms interval between readings seems to be best for 10Hz; slightly too slow
  //first bit is altitude mode (vs. barometer mode)

  //Altitude mode
  IIC_Write(0x26, 0b10111011); //bit 2 is one shot mode //0xB9 = 0b10111001
  IIC_Write(0x26, 0b10111001); //must clear oversampling (OST) bit, otherwise update will be once per second
  delay(550); //wait for measurement
  IIC_ReadData(); //
  altsmooth=Alt_Read();
  Serial.print("Altitude now: "); Serial.println(altsmooth);
  Serial.println("Done.");
}

void loop(){
  sensor_read_data();
  // your code here
}

void sensor_read_data(){
  // This function reads the altitude (or barometer) and temperature registers, then prints their values
  // variables for the calculations
  int m_temp;
  float l_temp;
  float altbaro, temperature;

  //One shot mode at 0b10101011 is slightly too fast, but better than wasting sensor cycles that increase precision
  //one reading seems to take 4ms (datasheet p.33);
  //oversampling at 32x=130ms interval between readings seems to be optimal for 10Hz
  #ifdef ALTMODE //Altitude mode
    IIC_Write(0x26, 0b10111011); //bit 2 is one shot mode //0xB9 = 0b10111001
    IIC_Write(0x26, 0b10111001); //must clear oversampling (OST) bit, otherwise update will be once per second
  #else //Barometer mode
    IIC_Write(0x26, 0b00111011); //bit 2 is one shot mode //0xB9 = 0b10111001
    IIC_Write(0x26, 0b00111001); //must clear oversampling (OST) bit, otherwise update will be once per second
  #endif
  delay(100); //read with 10Hz; drop this if calling from an outer loop

  IIC_ReadData(); //reads registers from the sensor
  m_temp = IICdata[3]; //temperature, degrees
  l_temp = (float)(IICdata[4]>>4)/16.0; //temperature, fraction of a degree
  temperature = (float)(m_temp + l_temp);

  #ifdef ALTMODE //converts byte data into float; change function to Alt_Read() or Baro_Read()
    altbaro = Alt_Read();
  #else
    altbaro = Baro_Read();
  #endif

  altsmooth=(altsmooth*3+altbaro)/4; //exponential smoothing to get a smooth time series

  Serial.print(altbaro); // in meters or Pascal
  Serial.print(",");
  Serial.print(altsmooth); // exponentially smoothed
  Serial.print(",");
  Serial.println(temperature); // in degrees C
}

float Baro_Read(){
  //this function takes values from the read buffer and converts them to pressure units
  unsigned long m_altitude = IICdata[0];
  unsigned long c_altitude = IICdata[1];
  float l_altitude = (float)(IICdata[2]>>4)/4; //dividing by 4, since two lowest bits are fractional value
  return((float)(m_altitude<<10 | c_altitude<<2)+l_altitude); //shifting 2 to the left to make room for LSB
}

float Alt_Read(){
  //Reads altitude data (if CTRL_REG1 is set to altitude mode)
  int m_altitude = IICdata[0];
  int c_altitude = IICdata[1];
  float l_altitude = (float)(IICdata[2]>>4)/16;
  return((float)((m_altitude << 8)|c_altitude) + l_altitude);
}

byte IIC_Read(byte regAddr){
  // This function reads one byte over I2C
  Wire.beginTransmission(SENSORADDRESS);
  Wire.write(regAddr); // Address of CTRL_REG1
  Wire.endTransmission(false); // Send data to I2C dev with option for a repeated start. Works in Arduino V1.0.1
  Wire.requestFrom(SENSORADDRESS, 1);
  return Wire.read();
}

void IIC_ReadData(){  //Read Altitude/Barometer and Temperature data (5 bytes)
  //This is faster than reading individual register, as the sensor automatically increments the register address,
  //so we just keep reading...
  byte i=0;
  Wire.beginTransmission(SENSORADDRESS);
  Wire.write(0x01); // Address of CTRL_REG1
  Wire.endTransmission(false);
  Wire.requestFrom(SENSORADDRESS,5); //read 5 bytes: 3 for altitude or pressure, 2 for temperature
  while(Wire.available()) IICdata[i++] = Wire.read();
}

void IIC_Write(byte regAddr, byte value){
  // This function writes one byto over I2C
  Wire.beginTransmission(SENSORADDRESS);
  Wire.write(regAddr);
  Wire.write(value);
  Wire.endTransmission(true);
}

Update (2013-06-26):
I have not tested the sensor’s stability and precision. However, I did use it in combination with an accelerometer to obtain vertical position and velocity. Because obtaining altitude and velocity involves trading off precision in altitude against precision in velocity, the graph below can only give an indication of the sensor’s precision. I am quite happy with the performance so far. This graph shows a ride in the lift from 20m down to about 5m (never mind the arbitrary altitude offset):

MPL3115A2_test

Time on the x-axis is in seconds. Altitude in meters is the red line with the scale on the left. The blue line is vertical velocity in m/s, measured on the right hand scale. Both values are calculated from acceleration and altitude in two loops, based on estimated average loop time of 22ms:

Altitude: zpos=0.95*(zpos + zvel*G_Dt + netacc/2 * G_Dt*G_Dt) + 0.05*curralt,

Velocity: zvel=0.95*(zvel+ netacc*G_Dt) + 0.05*(curralt-altold)/(5*G_Dt),

where netacc is vertical acceleration, G_Dt is about 22ms, curralt is the altimeter reading and altold is the previous altimeter reading. The factor of 5 accounts for the different update rate in accelerometer (once per loop) and altimeter (once every 5 loops) readings.

12 comments

Skip to comment form

    • Mike McRoberts on 20 April 2013 at 7:20 am

    Hi,

    I don’t understand one part of your code. According to the datasheet the pressure data is stored in 3 bytes and 18 bits make up the integer part and 2 bits make up the fractional part.

    In your code you take the MSB and bit shift it left 10 places and then the CSB you bit shift it left 4 places, meaning 1 bit overlaps. Also in the LSB bits 7 and 6 according to the datasheet make up the 17th and 18th bit of the integer part of the pressure reading. So surely you would need to add these two bits to the calculation? You seem to discard them.

    Finally, it is bits 5 and 4 that make up the fractional part of the pressure reading and yet you bit shift the LSB to the right 6 places meaning you are using bits 7 and 6 as the fractional part and not bits 5 and 4.

    I am confused as your code seems to differ from what the datasheet says the pressure reading is stored as.

    I would appreciate if you could clarify. Thanks.

    Mike

      • Henry on 21 April 2013 at 10:17 am
        Author

      Hi Mike,
      Thanks for pointing this out. The correct pressure calculation should be: Obtain the fractional part from shifting the LSB 4 bits to the right and dividing by 4 (this combines the two smallest integer bits and the fractional part). Shift the MSB by 10 bits to the left (this was correct) and the CSB by 2 (this was not), cast into float and add the fractional part. Correcting this error also solved the mysterious altitude offset problem, and the setup code got a little shorter.

      Henry

        • Alt on 22 July 2013 at 6:59 pm

        Henry.
        Looking at your code example, it seems that you are still missing a few bits to obtain the correct pressure value. As Mike has pointed, you need to add the two left-most bits (7-6) from the LSB to the integer part.

        Alt

          • Henry on 26 July 2013 at 7:37 pm
            Author

          Alt,
          The trick is to use bits 7-6 and the fractional bits 5-4 at the same time. Note that the LSB is shifted four places to the right and not six, which would put bits 7-6 in the right place. Dividing the result by four yields the correctly scaled bits 7-6 and also takes care of the fractional bits 5-4.

            • Alt on 11 August 2013 at 4:12 pm

            Oh, yes! Nice one!

    • Jack on 26 June 2013 at 8:27 am

    Hi,

    Do you finalise some test with MPL3115A2 ?
    What about the stability of the pressure measurement ?
    Did you try to check 1 meter difference altitude several time ? What about the result ?

    Thank a lot in advance for yours answers

    Jack

      • Henry on 26 June 2013 at 7:28 pm
        Author

      Hi Jack,
      I did not test the sensor’s precision on its own, because I needed a solution that gives me a vertical velocity as well. I added some data on this combined accelerometer + altimeter setup to the post. Maybe this helps. You can see on the right hand side of the graph that altitude readings are quite stable if the sensor is stationary. If I move it around a bit, the altitude drifts due to the accelerometer component in my filters. The magnitude of this drift is a function of the filter parameters and can be as low as about 0.3m. This value is claimed by the datasheet and looks about right.
      Best,
      Henry

    • Clauber on 26 June 2013 at 6:21 pm

    Hi,
    I am having an issue where I am trying to move this program from a Arduino Uno to a Arduino Pro Mini. The issue is that there are only A0-A3 analog pins. Nowhere in the code can I find a way to change the hardware connections from A4 & A5 to my avaliable pins. Does someone know how I can fix this? I assume it’s something to do with the memory hex values, but I don’t know how to lookup/understand those values.
    Thanks,
    C

      • Henry on 26 June 2013 at 7:36 pm
        Author

      Hi Clauber,
      This code actually runs on an Arduino Pro Mini. As far as the code is concerned, the Arduino Uno and Pro Mini are identical. The hardware difference is that you can find the A4 and A5 pins not on the edge of the board, but in the middle right next to the Atmega chip. Sometimes, even the A6 and A7 pins are brought out.
      Hope this helps.
      Henry

    • Kris Winer on 9 February 2014 at 2:45 am

    Hi Henry,

    I too built this sensor with a pro mini Arduino set up and plan to add an accelerometer for gravity and motion logging. Do you know how to use the FIFO capability of the Altimeter to get time vs. output (pressure, altitude, temperature, etc.) data over an arbitrary (limited by the sampling rate and device memory) time interval? I’d like to download the data held in the Altimeter’s FIFO buffers into an onboard SD card or laptop excel spreadsheet. Have you done something like this before or know someone who has? Thanks,

    Kris

      • Henry on 24 February 2014 at 8:35 pm
        Author

      Hi Kris,
      My goal for this project was to maximise sampling rate while keeping noise low in a real-time environment. I have not used the FIFO capability, which I assume would be useful if you wanted to reduce power consumption or processing overhead. The only application I can image for which FIFO would be useful is when you want to outsource timekeeping to the sensor or to let the Arduino sleep between measurements. If keeping a precise measurement interval is not critical, I would use the one-shot mode and let the Arduino sort out the timing and polling. I did not find any information which filtering algorithms are used by the chip, but this is probably only relevant if you want to maximise the time resolution and consider doing the filtering in your own code. From a measurement error point of view it seems reasonable to use the altimeter’s full time resolution, that is, running a loop close to the minimum time between samples for a given oversampling mode (page 33 on the datasheet).
      Cheers,
      Henry

    • Jeroen on 4 April 2018 at 9:23 pm

    Busy with a water rocket hobby project; I want the arduino to log the altitude.
    And yes… it works first time right; the arduino feather ‘adalogger’ can do the job!
    Excellent thanks a lot.

Comments have been disabled.