Friday 29 January 2016

Barometric Pressure, the BMP180 & Gambas

Some birds and other animals are thought to be sensitive to changes in atmospheric pressure.


This year I plan to monitor & record pressure, in addition to activity in and around my bird nesting boxes.


The BMP180 is a very small, low cost sensor which can be used to determine temperature and atmospheric pressure. ...looks like a great place to start!

I don't know if changes in air pressure affect bird behaviour such as feeding rate, but as I record the number of times the entrance detector is triggered when parents are feeding their chicks, I may be able to look for patterns.

The sensor


The Bosch BMP180 is very small, measuring approx 12mm x 10mm. It is also pretty cheap, I paid less than £3 from a supplier in China via Amazon.

It is very easy to connect to a Raspberry Pi via an I2C interface, using just 2 wires, plus 2 more for power.


BMP180          Pi GPIO
Vin                  pin 1   3V3
SDA                pin 3   serial data
SCL                 pin 5   serial clock
GND                pin 6   0V

For testing, I've just soldered on 4 jumper leads with female fittings to suit the Pi GPIO connector.

Preparing the Pi


There are a couple of things we need to do to the Pi before we can use the I2C interface.

From a terminal window run:-
sudo raspi-config

Enable the i2c interface via raspi-config > advanced Options

After a restart, open /etc/modules as root and copy the following 2 lines;
i2c-bcm2708
i2c-dev


Initially we can test the Pi-sensor comms using some i2c-tools:-
sudo apt-get install i2c-tools

For all Pi variants except the original model B try this:-
sudo i2cdetect 1

(for the old model B, use "0" in place of"1")

...this should give you the chip address: 77hex

Grab a copy of the memory map and save to a file:-
sudo i2cdump -y 1 0x77 > bmp180_mem.txt

This file may be useful later on if you need to check your code by comparing captured values with values in this file.

Using Gambas


We need to install wiringPi and Gambas as described in this earlier post.

Add the following declarations to the Gambas project:-

Library "/usr/local/lib/libwiringPi"
Public Extern wiringPiSetup() As Integer

'i2c stuff
Public Extern wiringPiI2CSetup(ChipID As Integer) As Integer
Public Extern wiringPiI2CRead(intFd As Integer) As Integer
Public Extern wiringPiI2CReadReg8(intHandle As Integer, intMem As Integer) As Integer
Public Extern wiringPiI2CReadReg16(intHandle As Integer, intMem As Integer) As Integer
Public Extern wiringPiI2CWriteReg8(intHandle As Integer, intMem As Integer, intData As Integer) As Integer
Public Extern wiringPiI2CWriteReg16(intHandle As Integer, intMem As Integer, intData As Integer) As Integer


The BMP180 datasheet details the memory locations we need to access, how to access them and the calculations required. There are a number of short and horrible variable names used in the documentation which I've adopted and adapted in an attempt to simplify diagnostics. However, variables like "X1" are not a great choice for Gambas, as the auto-complete feature in the IDE requires at least 3 characters before it kicks-in.

I also had a bit of bother with some variable types, so it is important to use the "short" type in some cases.

'BMP180 id is 0x77 = 119 decimal
Const CHIP_ADDR As Integer = 119  '0x77
Const CHIP_ID_ADDR As Integer = 208 '0xD0

Const AC1_ADDR As Integer = 170   '0xAA
Const AC2_ADDR As Integer = 172   '0xAC
Const AC3_ADDR As Integer = 174   '0xAE
Const AC4_ADDR As Integer = 176   '0xB0
Const AC5_ADDR As Integer = 178   '0xB2
Const AC6_ADDR As Integer = 180   '0xB4
Const B1_ADDR As Integer = 182   '0xB6
Const B2_ADDR As Integer = 184   '0xB8
Const MB_ADDR As Integer = 186   '0xBA
Const MC_ADDR As Integer = 188   '0xBC
Const MD_ADDR As Integer = 190   '0xBE

Const CTRL_TEMP As Integer = 46   '0x2E
Const CTRL_PRESSURE As Integer = 52   '0x34
Const CTRL_MEASURE As Integer = 244   '0xF4
Const ADC_OUT_MSB As Integer = 246      '0xF6
Const ADC_OUT_LSB As Integer = 247      '0xF7

Const START_OF_CONVERT As Integer = 32  'bit5 = 1
Const OVER_SAMPLING As Integer = 196      'bits 6 & 7 = 1

Public intDeviceHandle As Integer

'BMP180 calibration data
Public iAC1 As Short
Public iAC2 As Short
Public iAC3 As Short
Public iAC4 As Integer
Public iAC5 As Integer
Public iAC6 As Integer
Public iB1 As Short
Public iB2 As Short
Public iMB As Short
Public iMC As Short
Public iMD As Short

Public uncompTemperature As Long
Public uncompPressure As Long

Public lX1 As Long
Public lX2 As Long
Public lB5 As Long
Public lngTemp As Long

Public lB6 As Long
Public lX3 As Long
Public lB3 As Long
Public lB4 As Long
Public lB7 As Long
Public lPress As Long
Public lPressPascal As Long
Public fPressureMbar As Float

Public intLocalAltitude As Integer

The sensor contains calibration data which is required to calculate accurate temperature and pressure values.

Public Function GetCalibrationData() As Boolean
Dim msb As Integer
Dim lsb As Integer
 
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC1_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC1_ADDR + 1)
  iAC1 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC2_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC2_ADDR + 1)
  iAC2 = Shl(msb, 8) + lsb 
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC3_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC3_ADDR + 1)
  iAC3 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC4_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC4_ADDR + 1)
  iAC4 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC5_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC5_ADDR + 1)
  iAC5 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, AC6_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, AC6_ADDR + 1)
  iAC6 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, B1_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, B1_ADDR + 1)
  iB1 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, B2_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, B2_ADDR + 1)
  iB2 = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, MB_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, MB_ADDR + 1)
  iMB = (Shl(msb, 8) + lsb)  '* -1
  msb = wiringPiI2CReadReg8(intDeviceHandle, MC_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, MC_ADDR + 1)
  iMC = Shl(msb, 8) + lsb
  msb = wiringPiI2CReadReg8(intDeviceHandle, MD_ADDR)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, MD_ADDR + 1)
  iMD = Shl(msb, 8) + lsb
  Return True 


The temperature is calculated in the following fashion.

Public Function ReadTemperature() As Long
Dim msb As Short
Dim lsb As Short
 
  wiringPiI2CWriteReg8(intDeviceHandle, CTRL_MEASURE, CTRL_TEMP)
  Wait 0.005
  msb = wiringPiI2CReadReg8(intDeviceHandle, ADC_OUT_MSB)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, ADC_OUT_LSB)
  uncompTemperature = Shl(msb, 8) + lsb
  lX1 = (uncompTemperature - iAC6) * iAC5 / 2 ^ 15
  lX2 = (iMC * 2 ^ 11) / (lX1 + iMD)
  lB5 = lX1 + lX2
  lngTemp = (lb5 + 8) / 2 ^ 4
  Return lngTemp


The returned temperature value just needs to be divided by 10 to give degrees celsius.

If you thought that was bad, wait til you see the pressure calcs!

Public Function ReadPressure() As Long
Dim msb As Integer
Dim lsb As Integer
Dim xlsb As Integer
Dim oss As Short
 
  oss = 3
  wiringPiI2CWriteReg8(intDeviceHandle, CTRL_MEASURE, CTRL_PRESSURE + Shl(oss, 6))
  Wait 0.05
  msb = wiringPiI2CReadReg8(intDeviceHandle, ADC_OUT_MSB)
  lsb = wiringPiI2CReadReg8(intDeviceHandle, ADC_OUT_LSB)
  xlsb = wiringPiI2CReadReg8(intDeviceHandle, ADC_OUT_LSB + 1)
  uncompPressure = Shr(Shl(msb, 16) + Shl(lsb, 8) + xlsb, 8 - oss)
  lB6 = lB5 - 4000
  lX1 = (iB2 * (lB6 * lB6 / 2 ^ 12)) / 2 ^ 11
  lX2 = iAC2 * lB6 / 2 ^ 11
  lX3 = lX1 + lX2
  lB3 = (Shl(iAC1 * 4 + lX3, oss) + 2) / 4
  lX1 = iAC3 * lB6 / 2 ^ 13
  lX2 = (iB1 * (lB6 * lB6 / 2 ^ 12)) / 2 ^ 16
  lX3 = ((lX1 + lX2) + 2) / 2 ^ 2
  lB4 = iAC4 * (lX3 + 32768) / 2 ^ 15
  lB7 = (uncompPressure - lB3) * Shr(50000, oss)
  If lB7 < &H80000000 Then
    lPress = (lB7 * 2) / lB4
  Else
    lPress = (lB7 / lB4) * 2
  Endif
  lX1 = (lPress / 2 ^ 8) * (lPress / 2 ^ 8)
  lX1 = (lX1 * 3038) / 2 ^ 16
  lX2 = (-7357 * lPress) / 2 ^ 16
  lPressPascal = lPress + (lX1 + lX2 + 3791) / 2 ^ 4


The pressure reading needs to be adjusted to give the equivalent at sea level.

Public Function SeaLevelPressure() As Float
Dim intAltitude As Integer
Dim fTemp As Float
 
  intAltitude = sboxAltitude.Value
  If intAltitude < 0 Or intAltitude > 10000 Then
    intAltitude = 0
  Endif
  ftemp = intAltitude / 44330
  ftemp = 1 - ftemp
  ftemp = ftemp ^ 5.225
  ' Me.Text = ftemp
  Return 1 / ftemp



And now display temperature and atmospheric pressure:-

Public Sub tmrTemp_Timer()
Dim fSeaLevelCorrect As Float

  fSeaLevelCorrect = SeaLevelPressure()
  lblTemp.Text = CStr(ReadTemperature() / 10) & " 'C"
  fPressureMbar = Round(ReadPressure() / 100 * fSeaLevelCorrect, -2)
  lblPressure.Text = Format(fPressureMbar, "####.##") & " mbar"
End


The code snippets above were how the software looked just after I cracked the final problem, and started getting the right results on my test application. So there is plenty of scope to reduce and tidy the code. But I think it should provide the basics to allow anyone to get started.

My test app used for developing this code

In my case, I will be taking the relevant code to produce an app which records barometric pressure at regular intervals during the nesting season. This could be integrated into the BirdBox system. However, as this is a battery operated system, which is only turned on by activity, I would not get a continuous set of data.

So I'll probably fit the sensor to the "always-on" Raspberry Pi time server (this resets the bird box systems real time clock each time either of the two boxes are triggered and power up).

No comments:

Post a Comment