Skip to content

Commit

Permalink
BMP180 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vickash committed Sep 30, 2023
1 parent 76dc310 commit 49d0342
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 20 deletions.
2 changes: 1 addition & 1 deletion HARDWARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Polling and reading follow a call and response pattern.
| DHT 11/21/22 | :green_heart: | Digi In/Out | `Sensor::DHT` | Temp/RH
| SHT30 | :heart: | I2C | `Sensor::SHT30` | Temp/RH
| QMP6988 | :heart: | I2C | `Sensor::QMP6988` | Pressure
| BMP180 | :heart: | I2C | `Sensor::BMP180` | Temp/Press
| BMP180 | :green_heart: | I2C | `Sensor::BMP180` | Temp/Press
| BME280 | :green_heart: | I2C | `Sensor::BME280` | Temp/RH/Press
| BMP280 | :green_heart: | I2C | `Sensor::BMP280` | Temp/Press
| HTU21D | :green_heart: | I2C | `Sensor::HTU21D` | Temp/RH. User register read not implemented.
Expand Down
22 changes: 3 additions & 19 deletions examples/sensor/bme280.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,6 @@
require 'denko'

board = Denko::Board.new(Denko::Connection::Serial.new)

#
# Default pins for the I2C0 (first) interface on most chips:
#
# ATmega 328p: SDA = 'A4' SCL = 'A5' - Arduino Uno, Nano
# ATmega 32u4: SDA = 2 SCL = 3 - Arduino Leonardo, Pro Micro
# ATmega1280 / 2560: SDA = 20 SCL = 21 - Arduino Mega
# SAM3X8E: SDA = 20 SCL = 21 - Arduino Due
# SAMD21G18: SDA = 20 SCL = 21 - Arduino Zero, M0, M0 Pro
# ESP8266: SDA = 4 SCL = 5
# ESP32: SDA = 21 SCL = 22
# RP2040: SDA = 4 SCL = 5 - Raspberry Pi Pico (W)
#
# Only give the SDA pin of the I2C bus. SCL (clock) pin must be
# connected for it to work, but we don't need to control it.
#
bus = Denko::I2C::Bus.new(board: board, pin: :SDA)

sensor = Denko::Sensor::BME280.new(bus: bus, address: 0x76)
Expand Down Expand Up @@ -48,18 +32,18 @@ def display_reading(reading)
print "#{Time.now.strftime '%Y-%m-%d %H:%M:%S'} - "

# Temperature
formatted_temp = reading[:temperature].round(2).to_s.ljust(5, '0')
formatted_temp = reading[:temperature].to_f.round(2).to_s.ljust(5, '0')
print "Temperature: #{formatted_temp} \xC2\xB0C"

# Pressure
if reading[:pressure]
formatted_pressure = (reading[:pressure] / 101325).round(5).to_s.ljust(7, '0')
formatted_pressure = (reading[:pressure].to_f / 101325).round(5).to_s.ljust(7, '0')
print " | Pressure #{formatted_pressure} atm"
end

# Humidity
if reading[:humidity]
formatted_humidity = reading[:humidity].round(2).to_s.ljust(5, '0')
formatted_humidity = reading[:humidity].to_f.round(2).to_s.ljust(5, '0')
print " | Humidity #{formatted_humidity} %"
end

Expand Down
42 changes: 42 additions & 0 deletions examples/sensor/bmp180.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#
# Example using a BMP180 sensor over I2C, for temperature and pressure.
#
require 'bundler/setup'
require 'denko'

board = Denko::Board.new(Denko::Connection::Serial.new)
bus = Denko::I2C::Bus.new(board: board, pin: :SDA)
sensor = Denko::Sensor::BMP180.new(bus: bus, address: 0x77)

# Enable oversampling for the pressure sensor only (1,2,4, 8).
# sensor.pressure_samples = 8

def display_reading(reading)
# Time
print "#{Time.now.strftime '%Y-%m-%d %H:%M:%S'} - "

# Temperature
formatted_temp = reading[:temperature].to_f.round(2).to_s.ljust(5, '0')
print "Temperature: #{formatted_temp} \xC2\xB0C"

# Pressure
if reading[:pressure]
formatted_pressure = (reading[:pressure].to_f / 101325).round(5).to_s.ljust(7, '0')
print " | Pressure #{formatted_pressure} atm"
end

# Humidity
if reading[:humidity]
formatted_humidity = reading[:humidity].to_f.round(2).to_s.rjust(5, '0')
print " | Humidity #{formatted_humidity} %"
end

puts
end

# Poll the sensor and print readings.
sensor.poll(5) do |reading|
display_reading(reading)
end

sleep
1 change: 1 addition & 0 deletions lib/denko/sensor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Sensor
autoload :Humidity, "#{__dir__}/sensor/virtual"
autoload :DHT, "#{__dir__}/sensor/dht"
autoload :DS18B20, "#{__dir__}/sensor/ds18b20"
autoload :BMP180, "#{__dir__}/sensor/bmp180"
autoload :BME280, "#{__dir__}/sensor/bme280"
autoload :BMP280, "#{__dir__}/sensor/bme280"
autoload :HTU21D, "#{__dir__}/sensor/htu21d"
Expand Down
222 changes: 222 additions & 0 deletions lib/denko/sensor/bmp180.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
module Denko
module Sensor
class BMP180
include I2C::Peripheral
include Behaviors::Poller

# Write this to register 0xE0 for soft reset
SOFT_RESET = 0xB6

#
# Pressure Oversample Setting Values
#
# General formula:
# 2 ** n, where n is the decimal value of the bits, up to 8x pressure oversampling.
#
OVERSAMPLE_FACTORS = {
1 => 0b00,
2 => 0b01,
4 => 0b10,
8 => 0b11,
}

def before_initialize(options={})
@i2c_address = 0x77
super(options)
end

def after_initialize(options={})
super(options)

# Avoid repeated memory allocation for callback data and state.
@reading = { temperature: nil, pressure: nil }
self.state = { temperature: nil, pressure: nil }

# Default to start conversion off, reading temperature, no pressure oversampling.
@register = 0b00001110
@calibration_data_loaded = false
@oss = 0b00

# Temporary storage for raw bytes, since two I2C reads are needed for temperature and pressure.
@raw_bytes = [0, 0, 0, 0, 0]

soft_reset
end

#
# Configuration Methods
#
def soft_reset
i2c_write(SOFT_RESET)
end

attr_reader :measurement_time

def update_measurement_time
# Get oversample bits from current register setting.
oversample_exponent = (@register & 0b11000000) >> 6

# Calculate time in milliseconds.
extra_samples = (2 ** oversample_exponent) - 1
extra_time = extra_samples * 3
total_time = 4.5 + extra_time

# Sleep 1ms extra for safety and convert it to seconds.
@measurement_time = (total_time + 1) / 1000.0
end

def pressure_samples=(factor)
raise ArgumentError, "invalid oversampling factor: #{factor}" unless OVERSAMPLE_FACTORS.keys.include? factor
@oss = OVERSAMPLE_FACTORS[factor]
end

def write_settings
update_measurement_time
i2c_write [0xF4, @register]
end

#
# Reading Methods
#
def _start_read_temperature
@register = 0x2E
write_settings
end

def _start_read_pressure
@register = 0x34 | (@oss << 6)
write_settings
end

def _read
get_calibration_data unless calibration_data_loaded

_start_read_temperature
sleep(@measurement_time)
i2c_read 0xF6, 2

_start_read_pressure
sleep(@measurement_time)
i2c_read 0xF6, 3
end

def pre_callback_filter(data)
# Temperature is 2 bytes.
if data.length == 2
@raw_bytes[0] = data[0]
@raw_bytes[1] = data[1]
# Pressure is 3 bytes and triggers callbacks.
elsif data.length == 3
@raw_bytes[2] = data[0]
@raw_bytes[3] = data[1]
@raw_bytes[4] = data[2]
return decode_reading(@raw_bytes)
# Calibration data is 22 bytes.
elsif data.length == 22
process_calibration(data)
end

# Anything other than pressure avoids callbacks.
return nil
end

def update_state(reading)
# Checking for Hash ignores calibration data and nil.
if reading.class == Hash
@state_mutex.synchronize do
@state[:temperature] = reading[:temperature]
@state[:pressure] = reading[:pressure]
end
end
end

def [](key)
@state_mutex.synchronize do
return @state[key]
end
end

#
# Decoding Methods
#
def decode_reading(bytes)
temperature, b5 = decode_temperature(bytes)
@reading[:temperature] = temperature
@reading[:pressure] = decode_pressure(bytes, b5)
@reading
end

def decode_temperature(bytes)
# Temperature is bytes [0..2], MSB first.
ut = bytes[0] << 8 | bytes[1]

# Calibration compensation from datasheet
x1 = (ut - @calibration[:ac6]) * @calibration[:ac5] / 32768
x2 = (@calibration[:mc] * 2048) / (x1 + @calibration[:md])
b5 = x1 + x2

# 160 instead of 16 since datasheet calculates to 0.1 C units.
# Float to force the final value into float, but keep b5 integer for pressure.
temperature = (b5 + 8) / 160.0

# Return temperature and b5 for pressure calculation.
[temperature, b5]
end

def decode_pressure(bytes, b5)
# Pressure is bytes [2..3], MSB first.
up = ((bytes[2] << 16) | (bytes[3] << 8) | (bytes[4])) >> (8 - @oss)

# Calibration compensation from datasheet
b6 = b5 - 4000
x1 = (@calibration[:b2] * (b6 * b6 / 4096)) / 2048
x2 = @calibration[:ac2] * b6 / 2048
x3 = x1 + x2
b3 = (((@calibration[:ac1]*4 + x3) << @oss) + 2) / 4
x1 = @calibration[:ac3] * b6 / 8192
x2 = (@calibration[:b1] * (b6 * b6 / 4096)) / 65536
x3 = (x1 + x2 + 2) / 4
b4 = (@calibration[:ac4] * ((x3+32768) & 0xFFFF_FFFF)) / 32768
b7 = ((up & 0xFFFF_FFFF) - b3) * (50000 >> @oss)
if (b7 < 0x80000000)
p = (b7 * 2) / b4
else
p = (b7 / b4) * 2
end
x1 = (p / 256) * (p / 256)
x1 = (x1 * 3038) / 65536
x2 = (-7357 * p) / 65536
p = p + (x1 + x2 + 3791) / 16
end

#
# Calibration Methods
#
attr_reader :calibration_data_loaded

def get_calibration_data
# Calibration data is 22 bytes starting at address 0xAA.
read_using -> { i2c_read(0xAA, 22) }
end

def process_calibration(bytes)
if bytes
@calibration = {
ac1: bytes[0..1].pack('C*').unpack('s>')[0],
ac2: bytes[2..3].pack('C*').unpack('s>')[0],
ac3: bytes[4..5].pack('C*').unpack('s>')[0],
ac4: bytes[6..7].pack('C*').unpack('S>')[0],
ac5: bytes[8..9].pack('C*').unpack('S>')[0],
ac6: bytes[10..11].pack('C*').unpack('S>')[0],
b1: bytes[12..13].pack('C*').unpack('s>')[0],
b2: bytes[14..15].pack('C*').unpack('s>')[0],
mb: bytes[16..17].pack('C*').unpack('s>')[0],
mc: bytes[18..19].pack('C*').unpack('s>')[0],
md: bytes[20..21].pack('C*').unpack('s>')[0],
}
@calibration_data_loaded = true
end
end
end
end
end

0 comments on commit 49d0342

Please sign in to comment.