// Package bme280 provides a driver for the BME280 digital combined // humidity and pressure sensor by Bosch. // // Datasheet: // https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf package bme280 import ( "math" "time" "tinygo.org/x/drivers" "tinygo.org/x/drivers/internal/legacy" ) // calibrationCoefficients reads at startup and stores the calibration coefficients type calibrationCoefficients struct { t1 uint16 t2 int16 t3 int16 p1 uint16 p2 int16 p3 int16 p4 int16 p5 int16 p6 int16 p7 int16 p8 int16 p9 int16 h1 uint8 h2 int16 h3 uint8 h4 int16 h5 int16 h6 int8 } type Oversampling byte type Mode byte type FilterCoefficient byte type Period byte // Config contains settings for filtering, sampling, and modes of operation type Config struct { Pressure Oversampling Temperature Oversampling Humidity Oversampling Period Period Mode Mode IIR FilterCoefficient } // Device wraps an I2C connection to a BME280 device. type Device struct { bus drivers.I2C Address uint16 calibrationCoefficients calibrationCoefficients Config Config } // New creates a new BME280 connection. The I2C bus must already be // configured. // // This function only creates the Device object, it does not touch the device. func New(bus drivers.I2C) Device { return Device{ bus: bus, Address: Address, } } // ConfigureWithSettings sets up the device for communication and // read the calibration coefficients. // // The default configuration is the Indoor Navigation settings // from the BME280 datasheet. func (d *Device) Configure() { d.ConfigureWithSettings(Config{}) } // ConfigureWithSettings sets up the device for communication and // read the calibration coefficients. // // The default configuration if config is left at defaults is // the Indoor Navigation settings from the BME280 datasheet. func (d *Device) ConfigureWithSettings(config Config) { d.Config = config // If config is not initialized, use Indoor Navigation defaults. if d.Config == (Config{}) { d.Config = Config{ Mode: ModeNormal, Period: Period0_5ms, Temperature: Sampling2X, Humidity: Sampling1X, Pressure: Sampling16X, IIR: Coeff16, } } var data [24]byte err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CALIBRATION, data[:]) if err != nil { return } var h1 [1]byte err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_CALIBRATION_H1, h1[:]) if err != nil { return } var h2lsb [7]byte err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_CALIBRATION_H2LSB, h2lsb[:]) if err != nil { return } d.calibrationCoefficients.t1 = readUintLE(data[0], data[1]) d.calibrationCoefficients.t2 = readIntLE(data[2], data[3]) d.calibrationCoefficients.t3 = readIntLE(data[4], data[5]) d.calibrationCoefficients.p1 = readUintLE(data[6], data[7]) d.calibrationCoefficients.p2 = readIntLE(data[8], data[9]) d.calibrationCoefficients.p3 = readIntLE(data[10], data[11]) d.calibrationCoefficients.p4 = readIntLE(data[12], data[13]) d.calibrationCoefficients.p5 = readIntLE(data[14], data[15]) d.calibrationCoefficients.p6 = readIntLE(data[16], data[17]) d.calibrationCoefficients.p7 = readIntLE(data[18], data[19]) d.calibrationCoefficients.p8 = readIntLE(data[20], data[21]) d.calibrationCoefficients.p9 = readIntLE(data[22], data[23]) d.calibrationCoefficients.h1 = h1[0] d.calibrationCoefficients.h2 = readIntLE(h2lsb[0], h2lsb[1]) d.calibrationCoefficients.h3 = h2lsb[2] d.calibrationCoefficients.h6 = int8(h2lsb[6]) d.calibrationCoefficients.h4 = 0 + (int16(h2lsb[3]) << 4) | (int16(h2lsb[4] & 0x0F)) d.calibrationCoefficients.h5 = 0 + (int16(h2lsb[5]) << 4) | (int16(h2lsb[4]) >> 4) d.Reset() legacy.WriteRegister(d.bus, uint8(d.Address), CTRL_CONFIG, []byte{byte(d.Config.Period<<5) | byte(d.Config.IIR<<2)}) legacy.WriteRegister(d.bus, uint8(d.Address), CTRL_HUMIDITY_ADDR, []byte{byte(d.Config.Humidity)}) // Normal mode, start measuring now if d.Config.Mode == ModeNormal { legacy.WriteRegister(d.bus, uint8(d.Address), CTRL_MEAS_ADDR, []byte{ byte(d.Config.Temperature<<5) | byte(d.Config.Pressure<<2) | byte(d.Config.Mode)}) } } // Connected returns whether a BME280 has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { data := []byte{0} legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) return data[0] == CHIP_ID } // Reset the device func (d *Device) Reset() { legacy.WriteRegister(d.bus, uint8(d.Address), CMD_RESET, []byte{0xB6}) } // SetMode can set the device to Sleep, Normal or Forced mode // // Calling this method is optional, Configure can be used to set the // initial mode if no mode change is desired. This method is most // useful to switch between Sleep and Normal modes. func (d *Device) SetMode(mode Mode) { d.Config.Mode = mode legacy.WriteRegister(d.bus, uint8(d.Address), CTRL_MEAS_ADDR, []byte{ byte(d.Config.Temperature<<5) | byte(d.Config.Pressure<<2) | byte(d.Config.Mode)}) } // ReadTemperature returns the temperature in celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (int32, error) { data, err := d.readData() if err != nil { return 0, err } temp, _ := d.calculateTemp(data) return temp, nil } // ReadPressure returns the pressure in milli pascals mPa func (d *Device) ReadPressure() (int32, error) { data, err := d.readData() if err != nil { return 0, err } _, tFine := d.calculateTemp(data) pressure := d.calculatePressure(data, tFine) return pressure, nil } // ReadHumidity returns the relative humidity in hundredths of a percent func (d *Device) ReadHumidity() (int32, error) { data, err := d.readData() if err != nil { return 0, err } _, tFine := d.calculateTemp(data) humidity := d.calculateHumidity(data, tFine) return humidity, nil } // ReadAltitude returns the current altitude in meters based on the // current barometric pressure and estimated pressure at sea level. // Calculation is based on code from Adafruit BME280 library // // https://github.com/adafruit/Adafruit_BME280_Library func (d *Device) ReadAltitude() (alt int32, err error) { mPa, _ := d.ReadPressure() atmP := float32(mPa) / 100000 alt = int32(44330.0 * (1.0 - math.Pow(float64(atmP/SEALEVEL_PRESSURE), 0.1903))) return } // convert2Bytes converts two bytes to int32 func convert2Bytes(msb byte, lsb byte) int32 { return int32(readUint(msb, lsb)) } // convert3Bytes converts three bytes to int32 func convert3Bytes(msb byte, b1 byte, lsb byte) int32 { return int32(((((uint32(msb) << 8) | uint32(b1)) << 8) | uint32(lsb)) >> 4) } // readUint converts two bytes to uint16 func readUint(msb byte, lsb byte) uint16 { return (uint16(msb) << 8) | uint16(lsb) } // readUintLE converts two little endian bytes to uint16 func readUintLE(msb byte, lsb byte) uint16 { temp := readUint(msb, lsb) return (temp >> 8) | (temp << 8) } // readIntLE converts two little endian bytes to int16 func readIntLE(msb byte, lsb byte) int16 { return int16(readUintLE(msb, lsb)) } // readData does a burst read from 0xF7 to 0xF0 according to the datasheet // resulting in an slice with 8 bytes 0-2 = pressure / 3-5 = temperature / 6-7 = humidity func (d *Device) readData() (data [8]byte, err error) { if d.Config.Mode == ModeForced { // Write the CTRL_MEAS register to trigger a measurement legacy.WriteRegister(d.bus, uint8(d.Address), CTRL_MEAS_ADDR, []byte{ byte(d.Config.Temperature<<5) | byte(d.Config.Pressure<<2) | byte(d.Config.Mode)}) time.Sleep(d.measurementDelay()) } err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_PRESSURE, data[:]) if err != nil { println(err) return } return } // calculateTemp uses the data slice and applies calibrations values on it to convert the value to milli degrees // it also calculates the variable tFine which is used by the pressure and humidity calculation func (d *Device) calculateTemp(data [8]byte) (int32, int32) { rawTemp := convert3Bytes(data[3], data[4], data[5]) var1 := (((rawTemp >> 3) - (int32(d.calibrationCoefficients.t1) << 1)) * int32(d.calibrationCoefficients.t2)) >> 11 var2 := (((((rawTemp >> 4) - int32(d.calibrationCoefficients.t1)) * ((rawTemp >> 4) - int32(d.calibrationCoefficients.t1))) >> 12) * int32(d.calibrationCoefficients.t3)) >> 14 tFine := var1 + var2 T := (tFine*5 + 128) >> 8 return (10 * T), tFine } // calculatePressure uses the data slice and applies calibrations values on it to convert the value to milli pascals mPa func (d *Device) calculatePressure(data [8]byte, tFine int32) int32 { rawPressure := convert3Bytes(data[0], data[1], data[2]) var1 := int64(tFine) - 128000 var2 := var1 * var1 * int64(d.calibrationCoefficients.p6) var2 = var2 + ((var1 * int64(d.calibrationCoefficients.p5)) << 17) var2 = var2 + (int64(d.calibrationCoefficients.p4) << 35) var1 = ((var1 * var1 * int64(d.calibrationCoefficients.p3)) >> 8) + ((var1 * int64(d.calibrationCoefficients.p2)) << 12) var1 = ((int64(1) << 47) + var1) * int64(d.calibrationCoefficients.p1) >> 33 if var1 == 0 { return 0 // avoid exception caused by division by zero } p := int64(1048576 - rawPressure) p = (((p << 31) - var2) * 3125) / var1 var1 = (int64(d.calibrationCoefficients.p9) * (p >> 13) * (p >> 13)) >> 25 var2 = (int64(d.calibrationCoefficients.p8) * p) >> 19 p = ((p + var1 + var2) >> 8) + (int64(d.calibrationCoefficients.p7) << 4) p = (p / 256) return int32(1000 * p) } // calculateHumidity uses the data slice and applies calibrations values on it to convert the value to relative humidity in hundredths of a percent func (d *Device) calculateHumidity(data [8]byte, tFine int32) int32 { rawHumidity := convert2Bytes(data[6], data[7]) h := float32(tFine) - 76800 if h == 0 { println("invalid value") } var1 := float32(rawHumidity) - (float32(d.calibrationCoefficients.h4)*64.0 + (float32(d.calibrationCoefficients.h5) / 16384.0 * h)) var2 := float32(d.calibrationCoefficients.h2) / 65536.0 * (1.0 + float32(d.calibrationCoefficients.h6)/67108864.0*h* (1.0+float32(d.calibrationCoefficients.h3)/67108864.0*h)) h = var1 * var2 h = h * (1 - float32(d.calibrationCoefficients.h1)*h/524288) return int32(100 * h) } // measurementDelay returns how much time each measurement will take // on the device. // // This is used in forced mode to wait until a measurement is complete. func (d *Device) measurementDelay() time.Duration { const MeasOffset = 1250 const MeasDur = 2300 const HumMeasOffset = 575 const MeasScalingFactor = 1000 // delay is based on over-sampling rate - this table converts from // setting to number samples sampleRateConv := []int{0, 1, 2, 4, 8, 16} tempOsr := 16 if d.Config.Temperature <= Sampling16X { tempOsr = sampleRateConv[d.Config.Temperature] } presOsr := 16 if d.Config.Temperature <= Sampling16X { presOsr = sampleRateConv[d.Config.Pressure] } humOsr := 16 if d.Config.Temperature <= Sampling16X { humOsr = sampleRateConv[d.Config.Humidity] } max_delay := ((MeasOffset + (MeasDur * tempOsr) + ((MeasDur * presOsr) + HumMeasOffset) + ((MeasDur * humOsr) + HumMeasOffset)) / MeasScalingFactor) return time.Duration(max_delay) * time.Millisecond }