Thursday, July 3, 2014

Arduino (atmega328) Interrupt-based BMP085 (Barometric Pressure Sensor)

The BMP085 is a neat little sensor to measure atmospheric pressure, and ultimately altitude (which is the reason why I am using the damn thing in the first place).


The problem I found with the BMP085 library was that every time you want to read the temperature or pressure, you have to add a delay in your code and wait for the sensor to sample and return a valid value (the time delay will depend on the resolution you are using). The code below is part of the BMP085 library to read raw pressure from the barometer. Notice how after we request a pressure measurement, we wait for some time before the data can be read from the pressure register.

int32_t BMP085::readRawPressure(void) 
{
  uint32_t raw;

  /* Request pressure measurement from the BMP085 */
  I2C::write8( DEV_ADDR, CONTROL_REG, READPRESSURE_CMD + (oversampling << 6) );
  
  /* Wait until BMP085 finishes sampling pressure for you */
  if ( oversampling == ULTRALOWPOWER ) 
    delay(5);
  else if ( oversampling == STANDARD ) 
    delay(8);
  else if ( oversampling == HIGHRES ) 
    delay(14);
  else 
    delay(26);

  /* Read registers 0xF6, 0xF7, and 0xF8
   * UP = data(0xF6) << 16 | data(0xF7) << 8 | data(0xF8)
   */
  raw = I2C::read16( DEV_ADDR, PRESSURE_REG );
  raw <<= 8; 
  raw |= I2C::read8( DEV_ADDR, PRESSURE_REG + 2 ); 
  raw >>= (8 - oversampling);

  return raw;
}

The added delay would kill the rest of your code for that time. So, I'm sitting here thinking... interrupts anyone?

On the image above (right), the EOC pin (End of Conversion) essentially tells you when a conversion is done and the requested value is ready for you. This means that we can just request a value, continue on with our lives (but keep an eye on the EOC pin), and when that value is ready for you, then go ahead and read (no unnecessary delays). I made some modifications to the original library in order to include an interrupt-polling mechanism without the headache of timers, just good ol' pin change interrupts (PCINT).

The image below shows my configuration for the circuit diagram.



I am using Digital pin 4 (PD4) on my arduino to monitor the EOC state via Pin Change Interrupts. When we submit a measurement request, EOC goes LOW, and when the measurement is ready for us, EOC goes HIGH. We are interested in capturing RISING signal changes on PD4, then simply read from the BMP085 whatever value we had requested. The image below shows a high level representation of the interrupt-polling mechanism.




To allow pin change interrupts on PD4, we have to configure some registers. By looking at the atmega328 datasheet, setting PCIE2 bit in the PCICR register, effectively allows PortD to have pin change interrupts. However, we need to mask which pin within PortD we want to enable PCINT in. Digital Pin 4 is mapped to PCINT20. By setting this bit in the PCMSK2 register, we fully enable pin change interrupts on pin 4 (see below).

PCICR |= (1 << PCIE2 ); /* Allow PortD to have Pin Change Interrupts */
PCMSK2 |= (1 << PCINT20 ); /* Enable Pin Change Interrupts on PD4 */

The following routine is the interrupt handler, which is where we advance to the next stage in the polling process.

ISR( PCINT2_vect )
{
  ... In here we process the EOC event everytime there is a LOW-to-HIGH transition.
}

The following code snippet shows the pressure request we send to the BMP085. We keep track of the request through a status structure. This way, when we receive an EOC event, we can read the appropriate register from the barometer. We also set a busy flag to 1, since we don't want to send another request while we are waiting for a reply. This is valid for all requests.

void BMP085::readRawTempReq( void )
{
  if( !status.busy )
  {
    status.dataState = GET_RAW_TEMP_STATE;
    status.busy = 1;
    I2C::write8( DEV_ADDR, CONTROL_REG, READTEMP_CMD );
  }
}

The following code snippet shows the temperature request we send to the BMP085.

void BMP085::readRawPressureReq( void )
{
  if( !status.busy )
  {
    status.dataState = GET_RAW_PRESSURE_STATE;
    status.busy = 1;
    I2C::write8( DEV_ADDR, CONTROL_REG, READPRESSURE_CMD + (oversampling << 6) );
 }
}

This is the data structure used to keep track of the state of the barometer.
typedef struct _bmp_status
{
  uint8_t dataState;

  boolean busy;
  boolean eocReceived;
  
  int32_t rawPressure;
  int32_t compensatedPressure;
  uint16_t rawTemperature;
} BMP085_STATUS;

When we get the EOC event, we have to process it accordingly. The code below shows the eoc handler.

/* After read request was submitted, End Of Conversion event will trigger */
void BMP085::eocEvent( void )
{
  int32_t val;
  int32_t p;

  switch( status.dataState )
  {
    case GET_RAW_TEMP_STATE:
      status.rawTemperature = I2C::read16( DEV_ADDR, TEMPERATURE_REG );
      break;
    case GET_RAW_PRESSURE_STATE:
      val = I2C::read16( DEV_ADDR, PRESSURE_REG );
      val <<= 8;
      val |= I2C::read8( DEV_ADDR, PRESSURE_REG + 2 );
      val >>= (8 - oversampling);
      status.rawPressure = val;

      /* Calculate compensated pressure */
      status.compensatedPressure = calculateRealPressure( status.rawTemperature, status.rawPressure );
      break;
  }

  /* Have to clear eoc flag */
  status.eocReceived = 0;
  status.busy = 0;
}

Click HERE for the code on Github

No comments:

Post a Comment