Reading the BMP180 pressure sensor with an Attiny85

IMG_20160210_232932The BMP180 pressure sensor is a relatively cheap and popular sensor to read atmospheric pressure. Additionally it can read temperature.
If you want to use this sensor on an arduino, Adafruit has a library (for the BMP085 and the BMP180) that wil read it for you. However, the new library also needs their general ‘Sensor library’ and those are memory guzzlers. Perhaps OK on an Arduino, but not on an attiny. They do have one for the Tiny85 as well. Sparkfun also has a library  for the Arduino.
Furthermore, the BMP180 is an I2C device and I2C is not a standard on the Attiny series.

So, if you want to read the BMP180 sensor on an attiny, you would need to do some work yourself.
Fortunately, the datasheet is very very clear. Page 15 tells us exactly what to do.
The sequence is as follows:
1-Read the chip specific calibration data
2-Read the uncorrected temparature value
3-Read the uncorrected pressure value
4-Calculate true temperature
5-calculate true pressure

It also shows you what should be in a loop and what not:
reading the calibration data only needs to be done once and therefore goes in the ‘Setup’ routine. The rest is in a loop and therefore goes in the ‘loop’ routine.

So, programming is a breeze if you follow the flow chart on page 15…. we only need to ‘translate’ that into language the I2C protocol understands.
We therefore start the program with defining some general parameters:
For the Attiny there is the TinyWireM library that implements an I2C protocol on the attiny, so we need to load that library.
We need the I2C address of the BMP180 (which is 0x77), and we need to declare a whole bunch of variables. Most of the variables used will contain the chip specific calibration data that we will be reading from the chip’s EEPROM, we will need some variables for the various calculations and we will need some variables to contain the output (temperature and pressure)
To keep it easy, I have chosen names for the variables as mentioned in the datasheet.

So, the first lines of a program will look like this:

//The connection for  Attiny & BMP180 are  SDA pin 5 ,SCL pin 7 for I2C 
#include  <TinyWireM.h>
#define BMP180_ADDRESS 0x77  // I2C address of BMP180   
// define calibration data for temperature:
int ac1;
int ac2; 
int ac3; 
unsigned int ac4;
unsigned int ac5;
unsigned int ac6;
int b1; 
int b2;
int mb;
int mc;
int md;
long b5; 
//define variables for pressure and temperature calculation
long x1,x2;
//define variables for pressure calculation
long x3,b3,b6,p;
unsigned long b4,b7;
//define variables for temperature and pressure reading

short temperature;
long pressure;
const unsigned char OSS = 0;  // Oversampling Setting
/* blz 12 Datasheet
OSS=0 ultra Low Power Setting, 1 sample, 4.5 ms 3uA
OSS=1 Standard Power Setting, 2 samples, 7.5 ms 5uA
OSS=2 High Resolution,              4 samples, 13.5 ms 7uA
OSS=3 Ultra High Resolution,    2 samples, 25.5 ms 12uA
*/

Then we have to define the ‘Setup’ routine. Frankly, the only thing we have to do there is read the calibration data. To keep it simple, i will just call a procedure ‘bmp180ReadInt(address)’, which we then can implement later.
Our Setup therefore will look like this:

void setup() {
  TinyWireM.begin();
  // First read calibration data from EEPROM
  ac1 = bmp180ReadInt(0xAA);
  ac2 = bmp180ReadInt(0xAC);
  ac3 = bmp180ReadInt(0xAE);
  ac4 = bmp180ReadInt(0xB0);
  ac5 = bmp180ReadInt(0xB2);
  ac6 = bmp180ReadInt(0xB4);
  b1 = bmp180ReadInt(0xB6);
  b2 = bmp180ReadInt(0xB8);
  mb = bmp180ReadInt(0xBA);
  mc = bmp180ReadInt(0xBC);
  md = bmp180ReadInt(0xBE);

}

Ofcourse I could have just called 1 procedure and call that ‘bmp180ReadCalibration’ but that procedure then would do the same as I now defined already in the setup

The ‘loop’ procedure is equally simple. It is basically
Read uncorrected temperature
Correct that uncorrected temperature
Read uncorrected pressure
Correct that uncorrected pressure
But as no one is interested in the uncorrected data, we make that procedure:
Correct(Read Uncorrected temperature)
Correct(Read Uncorrected pressure)
like this:

void loop() {
 // first, read uncompensated temperature
 //temperature = bmp180ReadUT();
 //and then calculate calibrated temperature
 temperature = bmp180CorrectTemperature(bmp180ReadUT());
 // then , read uncompensated pressure
 //pressure = bmp180ReadUP();
 //and then calculate calibrated pressure
 pressure = bmp180CorrectPressure(bmp180ReadUP());
 
}

So that is it. We now only have to define the procedures that we call.
We will start with ‘bmp180ReadInt(address)’
This procedure will use the TinyWireM library to read an integer from a given address. In getting data from an I2C device, the general rule is to first write to that device to tell it what to do and then to read at a specific address for the outcome. As we will be reading from the EEPROM there is no specific command we have to send, other than to notify the I2C port where we want to be (at the I2C address of the chip) and send the address we want to read and how many bytes we want to read. We then combine those two butes in an integer and return that.
Our precedure will thus look like this:

int bmp180ReadInt(unsigned char address)
{
  unsigned char msb, lsb;
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(address);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BMP180_ADDRESS, 2);
  while(TinyWireM.available()<2);
  msb = TinyWireM.receive();
  lsb = TinyWireM.receive();
  return (int) msb<<8 | lsb;
}

The next procedure we need is to read the uncompensated temperature. To get that we have to first send the value of 0x2E to register 0xF4 and wait at least 4.5 msec. That is the time the chip needs to take 1 reading. After we waited we will read the uncompensated temperature from registers 0xF6 and 0xf7. That last read we do with the earlier defined ‘bmp180ReadInt’ procedure that reads 2 bytes and combines them into an integer.
The procedure thus will look like this:

unsigned int bmp180ReadUT()
{
  unsigned int ut;
  
  // Write 0x2E into Register 0xF4 and wait at least 4.5mS
  // This requests a temperature reading 
  // with results in 0xF6 and 0xF7
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF4);
  TinyWireM.send(0x2E);
  TinyWireM.endTransmission();
  
  // Wait at least 4.5ms
  delay(5);
  
  // Then read two bytes from registers 0xF6 (MSB) and 0xF7 (LSB)
  // and combine as unsigned integer
  ut = bmp180ReadInt(0xF6);
  return ut;
}

Subsequently we have to calculate the corrected temperature from the uncorrected temperature.
The datasheet defines that as follows:
UT=uncompensated temperature
X1=(UT-AC6)*AC5/2^15
X2=(MC * 2^11 /(X1+MD)
B5=X1+X2
T=(B5+8)/2^4
in software that looks like this

double bmp180CorrectTemperature(unsigned int ut)
{
  x1 = (((long)ut - (long)ac6)*(long)ac5) >> 15;
  x2 = ((long)mc << 11)/(x1 + md);  
  b5 = x1 + x2; 
  return (((b5 + 8)>>4));  
}

Well the temperature is done, now we need to read the uncompensated pressure. For that we need to write the value 0x34 in the register 0xF4, but we also have to set the value vor the oversampling rate.
The oversampling rate determines the amount of samples the chip needs to make before giving a result.
Page 4 of the datasheet tells we have 4 choices:
OSS=0 ultra Low Power Setting, 1 sample, 4.5 ms 3uA
OSS=1 Standard Power Setting, 2 samples, 7.5 ms 5uA
OSS=2 High Resolution, 4 samples, 13.5 ms 7uA
OSS=3 Ultra High Resolution, 12 samples, 25.5 ms 12uA
For this program I have chosen the OSS to be 0
The OSS contains bits 6 and 7 in register 0xF4. Bit 0-4 determine the control of the measurement.
if we write the value 0x34 that is in binary: 00110100. Bits 0 to 4 are not so important for now, but bit 5 will also be set and thus start the conversion. It will stay high during the conversion and reset to LOW after the conversion. In order to set the bits 6 and or 7 we have to left shift 6 the value of OSS. Suppose we had wanted to set OSS as 3. in binary that is 0b11 if we left shift 6 that, it will be 11000000 (=192d or 0xC0), which will set bits 6 and 7. 0x34+0xC0=0xF4=0b11110100 which as we can see is the same as 0x34 plus bit 6 and 7 set.
As we are using ‘0’ for the OSS value, both bit 6 and 7 will not be set.
after we start the conversion we have to wait between 4.5 and 25.5 msecs (depending on OSS). As we have OSS=0 we will wait 5msec.
Subsequently we will read 3 bytes as the temperature is a ‘long’ (4 bytes) not an integer, we will however only need 3 bytes.
With regard to the delay, it would be nice if we will define it as a dependency of the OSS so you do not need to manually change it when you change the OSS. The Adafruit library solevs this with some IF statements:
if (oversampling == BMP085_ULTRALOWPOWER)
delay(5);
else if (oversampling == BMP085_STANDARD)
delay(8);
else if (oversampling == BMP085_HIGHRES)
delay(14);
else
delay(26);
However, I hoped to find a formula that will determine it. As it isn’t a strict linear function, the closest one gets is the formula: 5+(OSS*5).
OSS=0->5
OSS=1->10
OSS=2->15
OSS=3->25
Well, I guess that would be close enough
The procedure is as follows

/-------------------------------------------
// Read the uncompensated pressure value
unsigned long bmp180ReadUP()
{
  unsigned char msb, lsb, xlsb;
  unsigned long up = 0;
  
  // Write 0x34+(OSS<<6) into register 0xF4
  // Request a pressure reading w/ oversampling setting
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF4);
  TinyWireM.send(0x34 + (OSS<<6));
  TinyWireM.endTransmission();
  
  // Wait for conversion, delay time dependent on OSS
  delay(5 + (5*OSS));
  
  // Read register 0xF6 (MSB), 0xF7 (LSB), and 0xF8 (XLSB)
  TinyWireM.beginTransmission(BMP180_ADDRESS);
  TinyWireM.send(0xF6);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BMP180_ADDRESS, 3);
  
  // Wait for data to become available
  while(TinyWireM.available() < 3)
    ;
  msb = TinyWireM.receive();
  lsb = TinyWireM.receive();
  xlsb = TinyWireM.receive();
  
  up = (((unsigned long) msb << 16) | ((unsigned long) lsb << 8) | (unsigned long) xlsb) >> (8-OSS);
  
  return up;
}

Now that is done, we need to correct the uncompensated pressure. The result will be in Pascal

double bmp180CorrectPressure(unsigned long up)
{ 
  b6 = b5 - 4000;
  // Calculate B3
  x1 = (b2 * (b6 * b6)>>12)>>11;
  x2 = (ac2 * b6)>>11;
  x3 = x1 + x2;
  b3 = (((((long)ac1)*4 + x3)<<OSS) + 2)>>2;
  
  // Calculate B4
  x1 = (ac3 * b6)>>13;
  x2 = (b1 * ((b6 * b6)>>12))>>16;
  x3 = ((x1 + x2) + 2)>>2;
  b4 = (ac4 * (unsigned long)(x3 + 32768))>>15;
  
  b7 = ((unsigned long)(up - b3) * (50000>>OSS));
  if (b7 < 0x80000000)
    p = (b7<<1)/b4;
  else
  p = (b7/b4)<<1;   
  x1 = (p>>8) * (p>>8);
  x1 = (x1 * 3038)>>16;
  x2 = (-7357 * p)>>16;
  p += (x1 + x2 + 3791)>>4;
  
  return p;
}


With the above program one can decide for oneself what to do with the found data: either  send it to a display, or perhaps send it via an RF link to a base station.
As said, the output of the pressure reading is in Pascal (Pa). hectoPascals are a more convenient unit. Some other units it can be  calculated in are:
1 hPa = 100 Pa = 1 mbar = 0.001 bar
1 hPa = 0.75006168 Torr
1 hPa = 0.01450377 psi (pounds per square inch)
1 hPa = 0.02953337 inHg (inches of mercury)
1 hpa = 0.00098692 atm (standard atmospheres)

One last advice still:  When you use the BMP180, remember it needs 3.3 Volt. 5 Volt will kill it. Using it on the I2C from a 5 Volt microcontroller shouldnot cause a problem though. Various break outboards actually do have a  3.3 Voltage regulator on it.

Warning
When I wanted to display the values found by the BMP180, I  grabbed a two wire LCD interface that I ahd build with a 164 Shift Register. I subsequently tried to figure out for several hours why I wasnt getting any decent read out. In fact, the read out didnt change wether I connected the BMP180 or not. After many many trials I started to suspect my display interface and decided to  hook up an I2C LCD. That worked like a charm.
The LiquidCrystal_I2C from Francisco Malpertida doesn’t work on the Attiny85. I used the classic LiquidCrystal_I2C that is adapted by Bro Hogan to work on the Attiny85 as well.
He did that by changing the line:

#include <Wire.h>
into
#if defined(__AVR_ATtiny85__) || (__AVR_ATtiny2313__)
#include "TinyWireM.h"      // include this if ATtiny85 or ATtiny2313
#else
#include <Wire.h>           // original lib include
#endif