From 5e39729535329fb5c0e1b478459c4316ae0b426a Mon Sep 17 00:00:00 2001 From: M-A Date: Fri, 25 Aug 2017 10:33:25 -0400 Subject: [PATCH] bmxx80: Merge bmp180 and bme280 and add support for bmp280 (#156) - cmd/bmxx80 now can read any of these 3 devices. - Rewrite the driver to support all 3 devices. More devices of the family (like the bme680) could be easily added. - Add -ia to bmx280smoketest Fix #155 --- devices/bme280/bme280.go | 644 ------------------ devices/bme280/bme280smoketest/README.md | 7 - devices/bmp180/bmp180.go | 284 -------- devices/bmxx80/bmp180.go | 115 ++++ devices/{bmp180 => bmxx80}/bmp180_test.go | 151 ++-- devices/bmxx80/bmx280.go | 209 ++++++ .../bme280_test.go => bmxx80/bmx280_test.go} | 240 ++++--- devices/bmxx80/bmx280smoketest/README.md | 7 + .../bmx280smoketest/bmx280smoketest.go} | 33 +- devices/bmxx80/bmxx80.go | 519 ++++++++++++++ 10 files changed, 1103 insertions(+), 1106 deletions(-) delete mode 100644 devices/bme280/bme280.go delete mode 100644 devices/bme280/bme280smoketest/README.md delete mode 100644 devices/bmp180/bmp180.go create mode 100644 devices/bmxx80/bmp180.go rename devices/{bmp180 => bmxx80}/bmp180_test.go (71%) create mode 100644 devices/bmxx80/bmx280.go rename devices/{bme280/bme280_test.go => bmxx80/bmx280_test.go} (76%) create mode 100644 devices/bmxx80/bmx280smoketest/README.md rename devices/{bme280/bme280smoketest/bme280smoketest.go => bmxx80/bmx280smoketest/bmx280smoketest.go} (83%) create mode 100644 devices/bmxx80/bmxx80.go diff --git a/devices/bme280/bme280.go b/devices/bme280/bme280.go deleted file mode 100644 index 29361c7..0000000 --- a/devices/bme280/bme280.go +++ /dev/null @@ -1,644 +0,0 @@ -// Copyright 2016 The Periph Authors. All rights reserved. -// Use of this source code is governed under the Apache License, Version 2.0 -// that can be found in the LICENSE file. - -// Package bme280 controls a Bosch BME280 device over I²C. -// -// Datasheet -// -// https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BME280_DS001-11.pdf -package bme280 - -import ( - "errors" - "fmt" - "log" - "sync" - "time" - - "periph.io/x/periph/conn" - "periph.io/x/periph/conn/i2c" - "periph.io/x/periph/conn/spi" - "periph.io/x/periph/devices" -) - -// Oversampling affects how much time is taken to measure each of temperature, -// pressure and humidity. -// -// Temperature must be measured for pressure and humidity to be measured. The -// duration is approximatively: -// duration_in_ms = 1 + 2*temp + 2*press+0.5 + 2*humidy+0.5 -// -// Using high oversampling and low standby results in highest power -// consumption, but this is still below 1mA so we generally don't care. -type Oversampling uint8 - -// Possible oversampling values. -// -// The higher the more time and power it takes to take a measurement. Even at -// 16x for all 3 sensors, it is less than 100ms albeit increased power -// consumption may increase the temperature reading. -const ( - Off Oversampling = 0 - O1x Oversampling = 1 - O2x Oversampling = 2 - O4x Oversampling = 3 - O8x Oversampling = 4 - O16x Oversampling = 5 -) - -const oversamplingName = "Off1x2x4x8x16x" - -var oversamplingIndex = [...]uint8{0, 3, 5, 7, 9, 11, 14} - -func (o Oversampling) String() string { - if o >= Oversampling(len(oversamplingIndex)-1) { - return fmt.Sprintf("Oversampling(%d)", o) - } - return oversamplingName[oversamplingIndex[o]:oversamplingIndex[o+1]] -} - -func (o Oversampling) asValue() int { - switch o { - case O1x: - return 1 - case O2x: - return 2 - case O4x: - return 4 - case O8x: - return 8 - case O16x: - return 16 - default: - return 0 - } -} - -// Filter specifies the internal IIR filter to get steadier measurements. -// -// Oversampling will get better measurements than filtering but at a larger -// power consumption cost, which may slightly affect temperature measurement. -type Filter uint8 - -// Possible filtering values. -// -// The higher the filter, the slower the value converges but the more stable -// the measurement is. -const ( - NoFilter Filter = 0 - F2 Filter = 1 - F4 Filter = 2 - F8 Filter = 3 - F16 Filter = 4 -) - -// Dev is a handle to an initialized bme280. -type Dev struct { - d conn.Conn - isSPI bool - opts Opts - measDelay time.Duration - c calibration - - mu sync.Mutex - stop chan struct{} - wg sync.WaitGroup -} - -func (d *Dev) String() string { - return fmt.Sprintf("BME280{%s}", d.d) -} - -// Sense requests a one time measurement as °C, kPa and % of relative humidity. -// -// The very first measurements may be of poor quality. -func (d *Dev) Sense(env *devices.Environment) error { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop != nil { - return wrap(errors.New("already sensing continuously")) - } - err := d.writeCommands([]byte{ - // ctrl_meas - 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(forced), - }) - if err != nil { - return wrap(err) - } - time.Sleep(d.measDelay) - for idle := false; !idle; { - if idle, err = d.isIdle(); err != nil { - return wrap(err) - } - } - return d.sense(env) -} - -// SenseContinuous returns measurements as °C, kPa and % of relative humidity -// on a continuous basis. -// -// The application must call Halt() to stop the sensing when done to stop the -// sensor and close the channel. -// -// It's the responsibility of the caller to retrieve the values from the -// channel as fast as possible, otherwise the interval may not be respected. -func (d *Dev) SenseContinuous(interval time.Duration) (<-chan devices.Environment, error) { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop != nil { - // Don't send the stop command to the device. - close(d.stop) - d.stop = nil - d.wg.Wait() - } - s := chooseStandby(interval - d.measDelay) - err := d.writeCommands([]byte{ - // config - 0xF5, byte(s)<<5 | byte(d.opts.Filter)<<2, - // ctrl_meas - 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(normal), - }) - if err != nil { - return nil, wrap(err) - } - sensing := make(chan devices.Environment) - d.stop = make(chan struct{}) - d.wg.Add(1) - go func() { - defer d.wg.Done() - defer close(sensing) - d.sensingContinuous(interval, sensing, d.stop) - }() - return sensing, nil -} - -// Halt stops the bme280 from acquiring measurements as initiated by -// SenseContinuous(). -// -// It is recommended to call this function before terminating the process to -// reduce idle power usage. -func (d *Dev) Halt() error { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop == nil { - return nil - } - close(d.stop) - d.stop = nil - d.wg.Wait() - // Page 27 (for register) and 12~13 section 3.3. - return d.writeCommands([]byte{ - // config - 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, - // ctrl_meas - 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), - }) -} - -// Opts is optional options to pass to the constructor. -// -// Recommended (and default) values are O4x for oversampling. -// -// Address can only used on creation of an I²C-device. Its default value is -// 0x76. It can be set to 0x77. Both values depend on HW configuration of the -// sensor's SDO pin. -// -// Filter is only used while using SenseContinuous(). -// -// Recommended sensing settings as per the datasheet: -// -// → Weather monitoring: manual sampling once per minute, all sensors O1x. -// Power consumption: 0.16µA, filter NoFilter. RMS noise: 3.3Pa / 30cm, 0.07%RH. -// -// → Humidity sensing: manual sampling once per second, pressure Off, humidity -// and temperature O1X, filter NoFilter. Power consumption: 2.9µA, 0.07%RH. -// -// → Indoor navigation: continuous sampling at 40ms with filter F16, pressure -// O16x, temperature O2x, humidity O1x, filter F16. Power consumption 633µA. -// RMS noise: 0.2Pa / 1.7cm. -// -// → Gaming: continuous sampling at 40ms with filter F16, pressure O4x, -// temperature O1x, humidity Off, filter F16. Power consumption 581µA. RMS -// noise: 0.3Pa / 2.5cm. -// -// See the datasheet for more details about the trade offs. -type Opts struct { - Temperature Oversampling - Pressure Oversampling - Humidity Oversampling - Filter Filter - Address uint16 -} - -func (o *Opts) delayTypical() time.Duration { - // Page 51. - µs := 1000 - if o.Temperature != Off { - µs += 2000 * o.Temperature.asValue() - } - if o.Pressure != Off { - µs += 2000*o.Pressure.asValue() + 500 - } - if o.Humidity != Off { - µs += 2000*o.Humidity.asValue() + 500 - } - return time.Microsecond * time.Duration(µs) -} - -// NewI2C returns an object that communicates over I²C to BME280 environmental -// sensor. -// -// It is recommended to call Halt() when done with the device so it stops -// sampling. -func NewI2C(b i2c.Bus, opts *Opts) (*Dev, error) { - addr := uint16(0x76) - if opts != nil { - switch opts.Address { - case 0x76, 0x77: - addr = opts.Address - case 0x00: - // do not do anything - default: - return nil, wrap(errors.New("given address not supported by device")) - } - } - d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, isSPI: false} - if err := d.makeDev(opts); err != nil { - return nil, err - } - return d, nil -} - -// NewSPI returns an object that communicates over SPI to BME280 environmental -// sensor. -// -// It is recommended to call Halt() when done with the device so it stops -// sampling. -// -// When using SPI, the CS line must be used. -func NewSPI(p spi.Port, opts *Opts) (*Dev, error) { - if opts != nil && opts.Address != 0 { - return nil, wrap(errors.New("do not use Address in SPI")) - } - // It works both in Mode0 and Mode3. - c, err := p.Connect(10000000, spi.Mode3, 8) - if err != nil { - return nil, wrap(err) - } - d := &Dev{d: c, isSPI: true} - if err := d.makeDev(opts); err != nil { - return nil, err - } - return d, nil -} - -// - -func (d *Dev) makeDev(opts *Opts) error { - if opts == nil { - opts = &defaults - } - if opts.Temperature == Off { - return wrap(errors.New("temperature measurement is required, use at least O1x")) - } - d.opts = *opts - d.measDelay = d.opts.delayTypical() - - // The device starts in 2ms as per datasheet. No need to wait for boot to be - // finished. - - var chipID [1]byte - // Read register 0xD0 to read the chip id. - if err := d.readReg(0xD0, chipID[:]); err != nil { - return err - } - if chipID[0] != 0x60 { - return wrap(fmt.Errorf("unexpected chip id %x; is this a BME280?", chipID[0])) - } - - // TODO(maruel): We may want to wait for isIdle(). - // Read calibration data t1~3, p1~9, 8bits padding, h1. - var tph [0xA2 - 0x88]byte - if err := d.readReg(0x88, tph[:]); err != nil { - return err - } - // Read calibration data h2~6 - var h [0xE8 - 0xE1]byte - if err := d.readReg(0xE1, h[:]); err != nil { - return err - } - d.c = newCalibration(tph[:], h[:]) - - config := []byte{ - // ctrl_meas; put it to sleep otherwise the config update may be ignored. - // This is really just in case the device was somehow put into normal but - // was not Halt'ed. - 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), - // ctrl_hum - 0xF2, byte(opts.Humidity), - // config - 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, - // As per page 25, ctrl_meas must be re-written last. - // ctrl_meas - 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), - } - if err := d.writeCommands(config[:]); err != nil { - return err - } - return nil -} - -// sense reads the device's registers. -// -// It must be called with d.mu lock held. -func (d *Dev) sense(env *devices.Environment) error { - // All registers must be read in a single pass, as noted at page 21, section - // 4.1. - // Pressure: 0xF7~0xF9 - // Temperature: 0xFA~0xFC - // Humidity: 0xFD~0xFE - buf := [0xFF - 0xF7]byte{} - if err := d.readReg(0xF7, buf[:]); err != nil { - return err - } - // These values are 20 bits as per doc. - pRaw := int32(buf[0])<<12 | int32(buf[1])<<4 | int32(buf[2])>>4 - tRaw := int32(buf[3])<<12 | int32(buf[4])<<4 | int32(buf[5])>>4 - // This value is 16 bits as per doc. - hRaw := int32(buf[6])<<8 | int32(buf[7]) - - t, tFine := d.c.compensateTempInt(tRaw) - env.Temperature = devices.Celsius(t * 10) - - p := d.c.compensatePressureInt64(pRaw, tFine) - env.Pressure = devices.KPascal((int32(p) + 127) / 256) - - h := d.c.compensateHumidityInt(hRaw, tFine) - env.Humidity = devices.RelativeHumidity((int32(h)*100 + 511) / 1024) - return nil -} - -func (d *Dev) sensingContinuous(interval time.Duration, sensing chan<- devices.Environment, stop <-chan struct{}) { - t := time.NewTicker(interval) - defer t.Stop() - - for { - // Do one initial sensing right away. - var e devices.Environment - d.mu.Lock() - err := d.sense(&e) - d.mu.Unlock() - if err != nil { - log.Printf("bme280: failed to sense: %v", err) - return - } - select { - case sensing <- e: - case <-stop: - return - } - select { - case <-stop: - return - case <-t.C: - } - } -} - -func (d *Dev) isIdle() (bool, error) { - // status - v := [1]byte{} - if err := d.readReg(0xF3, v[:]); err != nil { - return false, err - } - // Make sure bit 3 is cleared. Bit 0 is only important at device boot up. - return v[0]&8 == 0, nil -} - -func (d *Dev) readReg(reg uint8, b []byte) error { - // Page 32-33 - if d.isSPI { - // MSB is 0 for write and 1 for read. - read := make([]byte, len(b)+1) - write := make([]byte, len(read)) - // Rest of the write buffer is ignored. - write[0] = reg - if err := d.d.Tx(write, read); err != nil { - return wrap(err) - } - copy(b, read[1:]) - return nil - } - if err := d.d.Tx([]byte{reg}, b); err != nil { - return wrap(err) - } - return nil -} - -// writeCommands writes a command to the bme280. -// -// Warning: b may be modified! -func (d *Dev) writeCommands(b []byte) error { - if d.isSPI { - // Page 33; set RW bit 7 to 0. - for i := 0; i < len(b); i += 2 { - b[i] &^= 0x80 - } - } - if err := d.d.Tx(b, nil); err != nil { - return wrap(err) - } - return nil -} - -// - -// mode is the operating mode. -type mode byte - -const ( - sleep mode = 0 // no operation, all registers accessible, lowest power, selected after startup - forced mode = 1 // perform one measurement, store results and return to sleep mode - normal mode = 3 // perpetual cycling of measurements and inactive periods -) - -type status byte - -const ( - measuring status = 8 // set when conversion is running - imUpdate status = 1 // set when NVM data are being copied to image registers -) - -var defaults = Opts{ - Temperature: O4x, - Pressure: O4x, - Humidity: O4x, - Address: 0x76, -} - -// standby is the time the BME280 waits idle between measurements. This reduces -// power consumption when the host won't read the values as fast as the -// measurements are done. -type standby uint8 - -// Possible standby values, these determines the refresh rate. -const ( - s500us standby = 0 - s10ms standby = 6 - s20ms standby = 7 - s62ms standby = 1 - s125ms standby = 2 - s250ms standby = 3 - s500ms standby = 4 - s1s standby = 5 -) - -func chooseStandby(d time.Duration) standby { - switch { - case d < 10*time.Millisecond: - return s500us - case d < 20*time.Millisecond: - return s10ms - case d < 62500*time.Microsecond: - return s20ms - case d < 125*time.Millisecond: - return s62ms - case d < 250*time.Millisecond: - return s125ms - case d < 500*time.Millisecond: - return s250ms - case d < time.Second: - return s500ms - default: - return s1s - } -} - -// Register table: -// 0x00..0x87 -- -// 0x88..0xA1 Calibration data -// 0xA2..0xCF -- -// 0xD0 Chip id; reads as 0x60 -// 0xD1..0xDF -- -// 0xE0 Reset by writing 0xB6 to it -// 0xE1..0xF0 Calibration data -// 0xF1 -- -// 0xF2 ctrl_hum; ctrl_meas must be written to after for change to this register to take effect -// 0xF3 status -// 0xF4 ctrl_meas -// 0xF5 config -// 0xF6 -- -// 0xF7 press_msb -// 0xF8 press_lsb -// 0xF9 press_xlsb -// 0xFA temp_msb -// 0xFB temp_lsb -// 0xFC temp_xlsb -// 0xFD hum_msb -// 0xFE hum_lsb - -// https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf -// Page 23 - -// newCalibration parses calibration data from both buffers. -func newCalibration(tph, h []byte) (c calibration) { - c.t1 = uint16(tph[0]) | uint16(tph[1])<<8 - c.t2 = int16(tph[2]) | int16(tph[3])<<8 - c.t3 = int16(tph[4]) | int16(tph[5])<<8 - c.p1 = uint16(tph[6]) | uint16(tph[7])<<8 - c.p2 = int16(tph[8]) | int16(tph[9])<<8 - c.p3 = int16(tph[10]) | int16(tph[11])<<8 - c.p4 = int16(tph[12]) | int16(tph[13])<<8 - c.p5 = int16(tph[14]) | int16(tph[15])<<8 - c.p6 = int16(tph[16]) | int16(tph[17])<<8 - c.p7 = int16(tph[18]) | int16(tph[19])<<8 - c.p8 = int16(tph[20]) | int16(tph[21])<<8 - c.p9 = int16(tph[22]) | int16(tph[23])<<8 - c.h1 = uint8(tph[25]) - - c.h2 = int16(h[0]) | int16(h[1])<<8 - c.h3 = uint8(h[2]) - c.h4 = int16(h[3])<<4 | int16(h[4])&0xF - c.h5 = int16(h[4])>>4 | int16(h[5])<<4 - c.h6 = int8(h[6]) - - return c -} - -type calibration struct { - t1 uint16 - t2, t3 int16 - p1 uint16 - p2, p3, p4, p5, p6, p7, p8, p9 int16 - h2 int16 // Reordered for packing - h1, h3 uint8 - h4, h5 int16 - h6 int8 -} - -// Pages 23-24 - -// compensateTempInt returns temperature in °C, resolution is 0.01 °C. -// Output value of 5123 equals 51.23 C. -// -// raw has 20 bits of resolution. -func (c *calibration) compensateTempInt(raw int32) (int32, int32) { - x := ((raw>>3 - int32(c.t1)<<1) * int32(c.t2)) >> 11 - y := ((((raw>>4 - int32(c.t1)) * (raw>>4 - int32(c.t1))) >> 12) * int32(c.t3)) >> 14 - tFine := x + y - return (tFine*5 + 128) >> 8, tFine -} - -// compensatePressureInt64 returns pressure in Pa in Q24.8 format (24 integer -// bits and 8 fractional bits). Output value of 24674867 represents -// 24674867/256 = 96386.2 Pa = 963.862 hPa. -// -// raw has 20 bits of resolution. -func (c *calibration) compensatePressureInt64(raw, tFine int32) uint32 { - x := int64(tFine) - 128000 - y := x * x * int64(c.p6) - y += (x * int64(c.p5)) << 17 - y += int64(c.p4) << 35 - x = (x*x*int64(c.p3))>>8 + ((x * int64(c.p2)) << 12) - x = ((int64(1)<<47 + x) * int64(c.p1)) >> 33 - if x == 0 { - return 0 - } - p := ((((1048576 - int64(raw)) << 31) - y) * 3125) / x - x = (int64(c.p9) * (p >> 13) * (p >> 13)) >> 25 - y = (int64(c.p8) * p) >> 19 - return uint32(((p + x + y) >> 8) + (int64(c.p7) << 4)) -} - -// compensateHumidityInt returns humidity in %RH in Q22.10 format (22 integer -// and 10 fractional bits). Output value of 47445 represents 47445/1024 = -// 46.333% -// -// raw has 16 bits of resolution. -func (c *calibration) compensateHumidityInt(raw, tFine int32) uint32 { - x := tFine - 76800 - x1 := raw<<14 - int32(c.h4)<<20 - int32(c.h5)*x - x2 := (x1 + 16384) >> 15 - x3 := (x * int32(c.h6)) >> 10 - x4 := (x * int32(c.h3)) >> 11 - x5 := (x3 * (x4 + 32768)) >> 10 - x6 := ((x5+2097152)*int32(c.h2) + 8192) >> 14 - x = x2 * x6 - x = x - ((((x>>15)*(x>>15))>>7)*int32(c.h1))>>4 - if x < 0 { - return 0 - } - if x > 419430400 { - return 419430400 >> 12 - } - return uint32(x >> 12) -} - -func wrap(err error) error { - return fmt.Errorf("bme280: %v", err) -} - -var _ devices.Environmental = &Dev{} -var _ devices.Device = &Dev{} -var _ fmt.Stringer = &Dev{} diff --git a/devices/bme280/bme280smoketest/README.md b/devices/bme280/bme280smoketest/README.md deleted file mode 100644 index ac83acb..0000000 --- a/devices/bme280/bme280smoketest/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 'bme280' smoke test - -Verifies that two BME280, one over I²C, one over SPI, can read roughly the same -temperature, humidity and pressure. - -It can also be leveraged to record the I/O to write playback unit tests. It is a -good example to reuse to write other device driver unit test. diff --git a/devices/bmp180/bmp180.go b/devices/bmp180/bmp180.go deleted file mode 100644 index 78f052f..0000000 --- a/devices/bmp180/bmp180.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2017 The Periph Authors. All rights reserved. -// Use of this source code is governed under the Apache License, Version 2.0 -// that can be found in the LICENSE file. - -// Package bmp180 controls a Bosch BMP180 device over I²C. -// -// Datasheet -// -// https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMP180-DS000-121.pdf -// -// The font the official datasheet on page 15 is hard to read, a copy with -// readable text can be found here: -// -// https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf -// -// Notes on the datasheet -// -// The results of the calculations in the algorithm on page 15 are partly -// wrong. It looks like the original authors used non-integer calculations and -// some nubers were rounded. Take the results of the calculations with a grain -// of salt. -package bmp180 - -import ( - "encoding/binary" - "errors" - "fmt" - "log" - "sync" - "time" - - "periph.io/x/periph/conn/i2c" - "periph.io/x/periph/conn/mmr" - "periph.io/x/periph/devices" -) - -// Oversampling affects how much time is taken to measure pressure. -type Oversampling uint8 - -// Possible oversampling values. -// -// The higher the more time and power it takes to take a measurement. Even at -// 8x for pressure sensor, it is less than 30ms albeit at small increased power -// consumption, which may increase the temperature reading. -const ( - O1x Oversampling = 0 - O2x Oversampling = 1 - O4x Oversampling = 2 - O8x Oversampling = 3 -) - -const oversamplingName = "1x2x4x8x" - -var oversamplingIndex = [...]uint8{0, 2, 4, 6, 8} - -func (i Oversampling) String() string { - if i >= Oversampling(len(oversamplingIndex)-1) { - return fmt.Sprintf("Oversampling(%d)", i) - } - return oversamplingName[oversamplingIndex[i]:oversamplingIndex[i+1]] -} - -// Dev is a handle to a bmp180. -type Dev struct { - dev mmr.Dev8 - os Oversampling - cal calibration - - mu sync.Mutex - stop chan struct{} - wg sync.WaitGroup -} - -func (d *Dev) String() string { - return fmt.Sprintf("BMP180{%s}", d.dev.Conn) -} - -// Sense returns measurements as °C and kPa. -func (d *Dev) Sense(env *devices.Environment) error { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop != nil { - return wrap(errors.New("already sensing continuously")) - } - return d.sense(env) -} - -// SenseContinuous implements devices.Environmental. -func (d *Dev) SenseContinuous(interval time.Duration) (<-chan devices.Environment, error) { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop != nil { - close(d.stop) - d.stop = nil - d.wg.Wait() - } - - sensing := make(chan devices.Environment) - d.stop = make(chan struct{}) - d.wg.Add(1) - go func() { - defer d.wg.Done() - defer close(sensing) - d.sensingContinuous(interval, sensing, d.stop) - }() - return sensing, nil -} - -// Halt stops continuous reading. -func (d *Dev) Halt() error { - d.mu.Lock() - defer d.mu.Unlock() - if d.stop != nil { - close(d.stop) - d.stop = nil - d.wg.Wait() - } - return nil -} - -// New returns an object that communicates over I²C to BMP180 environmental -// sensor. -// -// The I²C bus frequency can be up to 3.4MHz. -func New(b i2c.Bus, os Oversampling) (d *Dev, err error) { - bus := &i2c.Dev{Bus: b, Addr: 0x77} - d = &Dev{dev: mmr.Dev8{Conn: bus, Order: binary.BigEndian}, os: os} - - // Confirm the chip ID. - id, err := d.dev.ReadUint8(0xD0) - if err != nil { - return nil, wrap(err) - } - if id != 0x55 { - return nil, wrap(fmt.Errorf("unexpected chip id 0x%x; is this a BMP180?", id)) - } - - // Read calibration data. - if err := d.dev.ReadStruct(0xAA, &d.cal); err != nil { - return nil, wrap(err) - } - if !d.cal.isValid() { - return nil, wrap(errors.New("calibration data is invalid")) - } - return d, nil -} - -// - -func (d *Dev) sense(env *devices.Environment) error { - // Request temperature convertion and read measurement. - if err := d.dev.WriteUint8(0xF4, 0x20|0x0E); err != nil { - return wrap(err) - } - time.Sleep(4500 * time.Microsecond) - ut, err := d.dev.ReadUint16(0xF6) - if err != nil { - return wrap(err) - } - temp := d.cal.compensateTemp(ut) - - // Request pressure conversion and read measurement. - if err := d.dev.WriteUint8(0xF4, 0x20|0x14|(uint8(d.os)<<6)); err != nil { - return wrap(err) - } - time.Sleep(pressureConvTime[d.os]) - var pressureBuf [3]byte - if err := d.dev.ReadStruct(0xF6, pressureBuf[:]); err != nil { - return wrap(err) - } - up := (int32(pressureBuf[0])<<16 + int32(pressureBuf[1])<<8 | int32(pressureBuf[2])) >> (8 - d.os) - pressure := d.cal.compensatePressure(up, int32(ut), d.os) - env.Temperature = devices.Celsius(temp * 100) - env.Pressure = devices.KPascal(pressure) - return nil -} - -func (d *Dev) sensingContinuous(interval time.Duration, sensing chan<- devices.Environment, stop <-chan struct{}) { - t := time.NewTicker(interval) - defer t.Stop() - - for { - // Do one initial sensing right away. - var e devices.Environment - d.mu.Lock() - err := d.sense(&e) - d.mu.Unlock() - if err != nil { - log.Printf("bmp180: failed to sense: %v", err) - return - } - select { - case sensing <- e: - case <-stop: - return - } - select { - case <-stop: - return - case <-t.C: - } - } -} - -// - -// Maximum conversion time for pressure. -var pressureConvTime = [...]time.Duration{ - 4500 * time.Microsecond, - 7500 * time.Microsecond, - 13500 * time.Microsecond, - 25500 * time.Microsecond, -} - -// calibration data read from the internal EEPROM (datasheet page 13). -type calibration struct { - AC1, AC2, AC3 int16 - AC4, AC5, AC6 uint16 - B1, B2 int16 - MB, MC, MD int16 -} - -func isValid(i int16) bool { - return i != 0 && i != ^int16(0) -} - -func isValidU(i uint16) bool { - return i != 0 && i != 0xFFFF -} - -// valid checks whether the calibration data is valid. -func (c *calibration) isValid() bool { - return isValid(c.AC1) && isValid(c.AC2) && isValid(c.AC3) && isValidU(c.AC4) && isValidU(c.AC5) && isValidU(c.AC6) && isValid(c.B1) && isValid(c.B2) && isValid(c.MB) && isValid(c.MC) && isValid(c.MD) -} - -// compensateTemp returns temperature in °C, resolution is 0.1 °C. -// Output value of 512 equals 51.2 C. -func (c *calibration) compensateTemp(raw uint16) int32 { - x1 := ((int64(raw) - int64(c.AC6)) * int64(c.AC5)) >> 15 - x2 := (int64(c.MC) << 11) / (x1 + int64(c.MD)) - b5 := x1 + x2 - t := (b5 + 8) >> 4 - return int32(t) -} - -// compensatePressure returns pressure in Pa. -func (c *calibration) compensatePressure(up, ut int32, os Oversampling) uint32 { - x1 := ((int64(ut) - int64(c.AC6)) * int64(c.AC5)) >> 15 - x2 := (int64(c.MC) * 2048) / (x1 + int64(c.MD)) - b5 := x1 + x2 - - b6 := b5 - 4000 - x1 = (int64(c.B2) * ((b6 * b6) >> 12)) >> 11 - x2 = int64(c.AC2) * b6 >> 11 - x3 := x1 + x2 - b3 := (((int64(c.AC1)*4 + x3) << uint(os)) + 2) / 4 - - x1 = (int64(c.AC3) * b6) >> 13 - x2 = (int64(c.B1) * ((b6 * b6) >> 12)) >> 16 - x3 = ((x1 + x2) + 2) / 4 - b4 := (int64(c.AC4) * (x3 + 32768)) >> 15 - b7 := (int64(up) - b3) * (50000 >> uint(os)) - - var p int64 - if b7 < 0x80000000 { - p = (b7 * 2) / b4 - } else { - p = (b7 / b4) * 2 - } - - x1 = (p >> 8) * (p >> 8) - x1 = (x1 * 3038) >> 16 - x2 = (-7357 * p) >> 16 - p = p + (x1+x2+3791)>>4 - return uint32(p) -} - -func wrap(err error) error { - return fmt.Errorf("bmp180: %v", err) -} - -var _ devices.Environmental = &Dev{} -var _ devices.Device = &Dev{} -var _ fmt.Stringer = &Dev{} diff --git a/devices/bmxx80/bmp180.go b/devices/bmxx80/bmp180.go new file mode 100644 index 0000000..e1b0e4b --- /dev/null +++ b/devices/bmxx80/bmp180.go @@ -0,0 +1,115 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package bmxx80 + +import ( + "encoding/binary" + "time" + + "periph.io/x/periph/devices" +) + +// sense180 reads the device's registers for bmp180. +// +// It must be called with d.mu lock held. +func (d *Dev) sense180(env *devices.Environment) error { + // Request temperature convertion and read measurement. + if err := d.writeCommands([]byte{0xF4, 0x20 | 0x0E}); err != nil { + return d.wrap(err) + } + time.Sleep(4500 * time.Microsecond) + var tempBuf [2]byte + if err := d.readReg(0xF6, tempBuf[:]); err != nil { + return d.wrap(err) + } + rawTemp := binary.BigEndian.Uint16(tempBuf[:]) + temp := d.cal180.compensateTemp(rawTemp) + + // Request pressure conversion and read measurement. + if err := d.writeCommands([]byte{0xF4, 0x20 | 0x14 | d.os<<6}); err != nil { + return d.wrap(err) + } + time.Sleep(pressureConvTime180[d.os]) + var pressureBuf [3]byte + if err := d.readReg(0xF6, pressureBuf[:]); err != nil { + return d.wrap(err) + } + up := (int32(pressureBuf[0])<<16 + int32(pressureBuf[1])<<8 | int32(pressureBuf[2])) >> (8 - d.os) + pressure := d.cal180.compensatePressure(up, int32(rawTemp), uint(d.os)) + env.Temperature = devices.Celsius(temp * 100) + env.Pressure = devices.KPascal(pressure) + return nil +} + +// pressureConvTime180 is the maximum conversion time for pressure. +var pressureConvTime180 = [...]time.Duration{ + 4500 * time.Microsecond, + 7500 * time.Microsecond, + 13500 * time.Microsecond, + 25500 * time.Microsecond, +} + +// calibration180 data read from the internal EEPROM (datasheet page 13). +type calibration180 struct { + AC1, AC2, AC3 int16 + AC4, AC5, AC6 uint16 + B1, B2 int16 + MB, MC, MD int16 +} + +func isValid(i int16) bool { + return i != 0 && i != ^int16(0) +} + +func isValidU(i uint16) bool { + return i != 0 && i != 0xFFFF +} + +// valid checks whether the calibration data is valid. +func (c *calibration180) isValid() bool { + return isValid(c.AC1) && isValid(c.AC2) && isValid(c.AC3) && isValidU(c.AC4) && isValidU(c.AC5) && isValidU(c.AC6) && isValid(c.B1) && isValid(c.B2) && isValid(c.MB) && isValid(c.MC) && isValid(c.MD) +} + +// compensateTemp returns temperature in °C, resolution is 0.1 °C. +// Output value of 512 equals 51.2 C. +func (c *calibration180) compensateTemp(raw uint16) int32 { + x1 := ((int64(raw) - int64(c.AC6)) * int64(c.AC5)) >> 15 + x2 := (int64(c.MC) << 11) / (x1 + int64(c.MD)) + b5 := x1 + x2 + t := (b5 + 8) >> 4 + return int32(t) +} + +// compensatePressure returns pressure in Pa. +func (c *calibration180) compensatePressure(up, ut int32, os uint) uint32 { + x1 := ((int64(ut) - int64(c.AC6)) * int64(c.AC5)) >> 15 + x2 := (int64(c.MC) * 2048) / (x1 + int64(c.MD)) + b5 := x1 + x2 + + b6 := b5 - 4000 + x1 = (int64(c.B2) * ((b6 * b6) >> 12)) >> 11 + x2 = int64(c.AC2) * b6 >> 11 + x3 := x1 + x2 + b3 := (((int64(c.AC1)*4 + x3) << os) + 2) / 4 + + x1 = (int64(c.AC3) * b6) >> 13 + x2 = (int64(c.B1) * ((b6 * b6) >> 12)) >> 16 + x3 = ((x1 + x2) + 2) / 4 + b4 := (int64(c.AC4) * (x3 + 32768)) >> 15 + b7 := (int64(up) - b3) * (50000 >> os) + + var p int64 + if b7 < 0x80000000 { + p = (b7 * 2) / b4 + } else { + p = (b7 / b4) * 2 + } + + x1 = (p >> 8) * (p >> 8) + x1 = (x1 * 3038) >> 16 + x2 = (-7357 * p) >> 16 + p = p + (x1+x2+3791)>>4 + return uint32(p) +} diff --git a/devices/bmp180/bmp180_test.go b/devices/bmxx80/bmp180_test.go similarity index 71% rename from devices/bmp180/bmp180_test.go rename to devices/bmxx80/bmp180_test.go index d3138bd..6acd118 100644 --- a/devices/bmp180/bmp180_test.go +++ b/devices/bmxx80/bmp180_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -package bmp180 +package bmxx80 import ( "testing" @@ -12,31 +12,33 @@ import ( "periph.io/x/periph/devices" ) -func TestNew_fail_read_chipid(t *testing.T) { +var opts180 = &Opts{Temperature: O1x, Pressure: O1x} + +func TestNew180_fail_read_chipid(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection read fail. }, DontPanic: true, } - if _, err := New(&bus, O1x); err == nil { + if _, err := NewI2C(&bus, 0x77, opts180); err == nil { t.Fatal("can't read chip ID") } } -func TestNew_bad_chipid(t *testing.T) { +func TestNew180_bad_chipid(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Bad Chip ID detection. - {Addr: 0x77, W: []byte{0xd0}, R: []byte{0x60}}, + {Addr: 0x77, W: []byte{0xd0}, R: []byte{0x61}}, }, } - if _, err := New(&bus, O1x); err == nil { + if _, err := NewI2C(&bus, 0x77, opts180); err == nil { t.Fatal("bad chip ID") } } -func TestNew_fail_calib(t *testing.T) { +func TestNew180_fail_calib(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -45,12 +47,12 @@ func TestNew_fail_calib(t *testing.T) { }, DontPanic: true, } - if _, err := New(&bus, O1x); err == nil { + if _, err := NewI2C(&bus, 0x77, opts180); err == nil { t.Fatal("can't read calibration") } } -func TestNew_bad_calib(t *testing.T) { +func TestNew180_bad_calib(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -64,61 +66,74 @@ func TestNew_bad_calib(t *testing.T) { }, DontPanic: true, } - if _, err := New(&bus, O1x); err == nil { + if _, err := NewI2C(&bus, 0x77, opts180); err == nil { t.Fatal("bad calibration") } } -func TestSense_success(t *testing.T) { - bus := i2ctest.Playback{ - Ops: []i2ctest.IO{ - // Chip ID detection. - {Addr: 0x77, W: []byte{0xd0}, R: []byte{0x55}}, - // Calibration data. - { - Addr: 0x77, - W: []byte{0xaa}, - R: []byte{35, 136, 251, 103, 199, 169, 135, 91, 98, 137, 80, 22, 25, 115, 0, 46, 128, 0, 209, 246, 10, 123}, +func TestSense180_success(t *testing.T) { + values := []struct { + o Oversampling + c byte + p devices.KPascal + }{ + {Oversampling(42), 0x34, 100567}, + {O1x, 0x34, 100567}, + {O2x, 0x74, 100567}, + {O4x, 0xB4, 100568}, + {O8x, 0xF4, 100568}, + } + for _, line := range values { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chip ID detection. + {Addr: 0x77, W: []byte{0xd0}, R: []byte{0x55}}, + // Calibration data. + { + Addr: 0x77, + W: []byte{0xaa}, + R: []byte{35, 136, 251, 103, 199, 169, 135, 91, 98, 137, 80, 22, 25, 115, 0, 46, 128, 0, 209, 246, 10, 123}, + }, + // Request temperature. + {Addr: 0x77, W: []byte{0xF4, 0x2E}}, + // Read temperature. + {Addr: 0x77, W: []byte{0xF6}, R: []byte{0x71, 0xBf}}, + // Request pressure. + {Addr: 0x77, W: []byte{0xF4, line.c}}, + // Read pressure. + {Addr: 0x77, W: []byte{0xF6}, R: []byte{0xAb, 0x96, 0}}, }, - // Request temperature. - {Addr: 0x77, W: []byte{0xF4, 0x2E}}, - // Read temperature. - {Addr: 0x77, W: []byte{0xF6}, R: []byte{0x71, 0xBf}}, - // Request pressure. - {Addr: 0x77, W: []byte{0xF4, 0x34}}, - // Read pressure. - {Addr: 0x77, W: []byte{0xF6}, R: []byte{0xAb, 0x96, 0}}, - }, - } - dev, err := New(&bus, O1x) - if err != nil { - t.Fatal(err) - } - if s := dev.String(); s != "BMP180{playback(119)}" { - t.Fatal(s) - } - env := devices.Environment{} - if err := dev.Sense(&env); err != nil { - t.Fatal(err) - } - if env.Temperature != 25300 { - t.Fatalf("temp %d", env.Temperature) - } - if env.Pressure != 100567 { - t.Fatalf("pressure %d", env.Pressure) - } - if env.Humidity != 0 { - t.Fatalf("humidity %d", env.Humidity) - } - if err := dev.Halt(); err != nil { - t.Fatal(err) - } - if err := bus.Close(); err != nil { - t.Fatal(err) + } + dev, err := NewI2C(&bus, 0x77, &Opts{Pressure: line.o}) + if err != nil { + t.Fatal(err) + } + if s := dev.String(); s != "BMP180{playback(119)}" { + t.Fatal(s) + } + env := devices.Environment{} + if err := dev.Sense(&env); err != nil { + t.Fatal(err) + } + if env.Temperature != 25300 { + t.Fatalf("temp %d", env.Temperature) + } + if env.Pressure != line.p { + t.Fatalf("pressure %d", env.Pressure) + } + if env.Humidity != 0 { + t.Fatalf("humidity %d", env.Humidity) + } + if err := dev.Halt(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } } } -func TestSense_fail_1(t *testing.T) { +func TestSense180_fail_1(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -133,7 +148,7 @@ func TestSense_fail_1(t *testing.T) { }, DontPanic: true, } - dev, err := New(&bus, O1x) + dev, err := NewI2C(&bus, 0x77, opts180) if err != nil { t.Fatal(err) } @@ -146,7 +161,7 @@ func TestSense_fail_1(t *testing.T) { } } -func TestSense_fail_2(t *testing.T) { +func TestSense180_fail_2(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -163,7 +178,7 @@ func TestSense_fail_2(t *testing.T) { }, DontPanic: true, } - dev, err := New(&bus, O1x) + dev, err := NewI2C(&bus, 0x77, opts180) if err != nil { t.Fatal(err) } @@ -176,7 +191,7 @@ func TestSense_fail_2(t *testing.T) { } } -func TestSense_fail_3(t *testing.T) { +func TestSense180_fail_3(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -195,7 +210,7 @@ func TestSense_fail_3(t *testing.T) { }, DontPanic: true, } - dev, err := New(&bus, O1x) + dev, err := NewI2C(&bus, 0x77, opts180) if err != nil { t.Fatal(err) } @@ -208,7 +223,7 @@ func TestSense_fail_3(t *testing.T) { } } -func TestSense_fail_4(t *testing.T) { +func TestSense180_fail_4(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -229,7 +244,7 @@ func TestSense_fail_4(t *testing.T) { }, DontPanic: true, } - dev, err := New(&bus, O1x) + dev, err := NewI2C(&bus, 0x77, opts180) if err != nil { t.Fatal(err) } @@ -242,7 +257,7 @@ func TestSense_fail_4(t *testing.T) { } } -func TestSenseContinuous_success(t *testing.T) { +func TestSenseContinuous180_success(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -272,7 +287,7 @@ func TestSenseContinuous_success(t *testing.T) { }, DontPanic: true, } - dev, err := New(&bus, O1x) + dev, err := NewI2C(&bus, 0x77, opts180) if err != nil { t.Fatal(err) } @@ -318,6 +333,7 @@ func TestSenseContinuous_success(t *testing.T) { } } +/* func TestOversampling(t *testing.T) { data := []struct { o Oversampling @@ -336,9 +352,10 @@ func TestOversampling(t *testing.T) { } } } +*/ -func TestCompensate(t *testing.T) { - c := calibration{ +func TestCompensate180(t *testing.T) { + c := calibration180{ AC1: 408, AC2: -72, AC3: -14383, diff --git a/devices/bmxx80/bmx280.go b/devices/bmxx80/bmx280.go new file mode 100644 index 0000000..eca708a --- /dev/null +++ b/devices/bmxx80/bmx280.go @@ -0,0 +1,209 @@ +// Copyright 2016 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package bmxx80 + +import ( + "time" + + "periph.io/x/periph/devices" +) + +// sense280 reads the device's registers for bme280/bmp280. +// +// It must be called with d.mu lock held. +func (d *Dev) sense280(env *devices.Environment) error { + // All registers must be read in a single pass, as noted at page 21, section + // 4.1. + // Pressure: 0xF7~0xF9 + // Temperature: 0xFA~0xFC + // Humidity: 0xFD~0xFE + buf := [8]byte{} + b := buf[:] + if !d.isBME { + b = buf[:6] + } + if err := d.readReg(0xF7, b); err != nil { + return err + } + // These values are 20 bits as per doc. + pRaw := int32(buf[0])<<12 | int32(buf[1])<<4 | int32(buf[2])>>4 + tRaw := int32(buf[3])<<12 | int32(buf[4])<<4 | int32(buf[5])>>4 + + t, tFine := d.cal280.compensateTempInt(tRaw) + env.Temperature = devices.Celsius(t * 10) + + if d.opts.Pressure != Off { + p := d.cal280.compensatePressureInt64(pRaw, tFine) + env.Pressure = devices.KPascal((int32(p) + 127) / 256) + } + + if d.opts.Humidity != Off { + // This value is 16 bits as per doc. + hRaw := int32(buf[6])<<8 | int32(buf[7]) + h := d.cal280.compensateHumidityInt(hRaw, tFine) + env.Humidity = devices.RelativeHumidity((int32(h)*100 + 511) / 1024) + } + return nil +} + +func (d *Dev) isIdle280() (bool, error) { + // status + v := [1]byte{} + if err := d.readReg(0xF3, v[:]); err != nil { + return false, err + } + // Make sure bit 3 is cleared. Bit 0 is only important at device boot up. + return v[0]&8 == 0, nil +} + +// mode is the operating mode. +type mode byte + +const ( + sleep mode = 0 // no operation, all registers accessible, lowest power, selected after startup + forced mode = 1 // perform one measurement, store results and return to sleep mode + normal mode = 3 // perpetual cycling of measurements and inactive periods +) + +// standby is the time the BMx280 waits idle between measurements. This reduces +// power consumption when the host won't read the values as fast as the +// measurements are done. +type standby uint8 + +// Possible standby values, these determines the refresh rate. +const ( + s500us standby = 0 + s10msBME standby = 6 + s20msBME standby = 7 + s62ms standby = 1 + s125ms standby = 2 + s250ms standby = 3 + s500ms standby = 4 + s1s standby = 5 + s2sBMP standby = 6 + s4sBMP standby = 7 +) + +func chooseStandby(isBME bool, d time.Duration) standby { + switch { + case d < 10*time.Millisecond: + return s500us + case isBME && d < 20*time.Millisecond: + return s10msBME + case isBME && d < 62500*time.Microsecond: + return s20msBME + case d < 125*time.Millisecond: + return s62ms + case d < 250*time.Millisecond: + return s125ms + case d < 500*time.Millisecond: + return s250ms + case d < time.Second: + return s500ms + case d < 2*time.Second: + return s1s + case !isBME && d < 4*time.Second: + return s2sBMP + default: + if isBME { + return s1s + } + return s4sBMP + } +} + +// newCalibration parses calibration data from both buffers. +func newCalibration(tph, h []byte) (c calibration280) { + c.t1 = uint16(tph[0]) | uint16(tph[1])<<8 + c.t2 = int16(tph[2]) | int16(tph[3])<<8 + c.t3 = int16(tph[4]) | int16(tph[5])<<8 + c.p1 = uint16(tph[6]) | uint16(tph[7])<<8 + c.p2 = int16(tph[8]) | int16(tph[9])<<8 + c.p3 = int16(tph[10]) | int16(tph[11])<<8 + c.p4 = int16(tph[12]) | int16(tph[13])<<8 + c.p5 = int16(tph[14]) | int16(tph[15])<<8 + c.p6 = int16(tph[16]) | int16(tph[17])<<8 + c.p7 = int16(tph[18]) | int16(tph[19])<<8 + c.p8 = int16(tph[20]) | int16(tph[21])<<8 + c.p9 = int16(tph[22]) | int16(tph[23])<<8 + c.h1 = uint8(tph[25]) + + c.h2 = int16(h[0]) | int16(h[1])<<8 + c.h3 = uint8(h[2]) + c.h4 = int16(h[3])<<4 | int16(h[4])&0xF + c.h5 = int16(h[4])>>4 | int16(h[5])<<4 + c.h6 = int8(h[6]) + + return c +} + +type calibration280 struct { + t1 uint16 + t2, t3 int16 + p1 uint16 + p2, p3, p4, p5, p6, p7, p8, p9 int16 + h2 int16 // Reordered for packing + h1, h3 uint8 + h4, h5 int16 + h6 int8 +} + +// Pages 23-24 + +// compensateTempInt returns temperature in °C, resolution is 0.01 °C. +// Output value of 5123 equals 51.23 C. +// +// raw has 20 bits of resolution. +func (c *calibration280) compensateTempInt(raw int32) (int32, int32) { + x := ((raw>>3 - int32(c.t1)<<1) * int32(c.t2)) >> 11 + y := ((((raw>>4 - int32(c.t1)) * (raw>>4 - int32(c.t1))) >> 12) * int32(c.t3)) >> 14 + tFine := x + y + return (tFine*5 + 128) >> 8, tFine +} + +// compensatePressureInt64 returns pressure in Pa in Q24.8 format (24 integer +// bits and 8 fractional bits). Output value of 24674867 represents +// 24674867/256 = 96386.2 Pa = 963.862 hPa. +// +// raw has 20 bits of resolution. +func (c *calibration280) compensatePressureInt64(raw, tFine int32) uint32 { + x := int64(tFine) - 128000 + y := x * x * int64(c.p6) + y += (x * int64(c.p5)) << 17 + y += int64(c.p4) << 35 + x = (x*x*int64(c.p3))>>8 + ((x * int64(c.p2)) << 12) + x = ((int64(1)<<47 + x) * int64(c.p1)) >> 33 + if x == 0 { + return 0 + } + p := ((((1048576 - int64(raw)) << 31) - y) * 3125) / x + x = (int64(c.p9) * (p >> 13) * (p >> 13)) >> 25 + y = (int64(c.p8) * p) >> 19 + return uint32(((p + x + y) >> 8) + (int64(c.p7) << 4)) +} + +// compensateHumidityInt returns humidity in %RH in Q22.10 format (22 integer +// and 10 fractional bits). Output value of 47445 represents 47445/1024 = +// 46.333% +// +// raw has 16 bits of resolution. +func (c *calibration280) compensateHumidityInt(raw, tFine int32) uint32 { + x := tFine - 76800 + x1 := raw<<14 - int32(c.h4)<<20 - int32(c.h5)*x + x2 := (x1 + 16384) >> 15 + x3 := (x * int32(c.h6)) >> 10 + x4 := (x * int32(c.h3)) >> 11 + x5 := (x3 * (x4 + 32768)) >> 10 + x6 := ((x5+2097152)*int32(c.h2) + 8192) >> 14 + x = x2 * x6 + x = x - ((((x>>15)*(x>>15))>>7)*int32(c.h1))>>4 + if x < 0 { + return 0 + } + if x > 419430400 { + return 419430400 >> 12 + } + return uint32(x >> 12) +} diff --git a/devices/bme280/bme280_test.go b/devices/bmxx80/bmx280_test.go similarity index 76% rename from devices/bme280/bme280_test.go rename to devices/bmxx80/bmx280_test.go index 93e981e..2301180 100644 --- a/devices/bme280/bme280_test.go +++ b/devices/bmxx80/bmx280_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -package bme280 +package bmxx80 import ( "errors" @@ -22,7 +22,7 @@ import ( ) // Real data extracted from a device. -var calib = calibration{ +var calib280 = calibration280{ t1: 28176, t2: 26220, t3: 350, @@ -43,7 +43,7 @@ var calib = calibration{ h6: 30, } -func TestSPISense_success(t *testing.T) { +func TestSPISenseBME280_success(t *testing.T) { s := spitest.Playback{ Playback: conntest.Playback{ Ops: []conntest.IO{ @@ -57,7 +57,7 @@ func TestSPISense_success(t *testing.T) { W: []byte{0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, R: []byte{0x00, 0xC9, 0x6C, 0x63, 0x65, 0x32, 0x00, 0x77, 0x93, 0x98, 0xD5, 0xD0, 0x0B, 0x67, 0x23, 0xBA, 0x00, 0xF9, 0xFF, 0xAC, 0x26, 0x0A, 0xD8, 0xBD, 0x10, 0x00, 0x4B}, }, - // Calibration data. + // Calibration data humidity. { W: []byte{0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, R: []byte{0x00, 0x5C, 0x01, 0x00, 0x15, 0x0F, 0x00, 0x1E}, @@ -109,16 +109,13 @@ func TestSPISense_success(t *testing.T) { } } -func TestNewSPI_fail(t *testing.T) { - if d, err := NewSPI(&spiFail{}, nil); d != nil || err == nil { - t.Fatal("Connect() have failed") - } - if d, err := NewSPI(&spiFail{}, &Opts{Address: 1}); d != nil || err == nil { - t.Fatal("Address can't be used with SPI") +func TestNewSPIBME280_fail_Connect(t *testing.T) { + if dev, err := NewSPI(&spiFail{}, nil); dev != nil || err == nil { + t.Fatal("read failed") } } -func TestNewSPI_fail_len(t *testing.T) { +func TestNewSPIBME280_fail_len(t *testing.T) { s := spitest.Playback{ Playback: conntest.Playback{ Ops: []conntest.IO{ @@ -141,7 +138,7 @@ func TestNewSPI_fail_len(t *testing.T) { } } -func TestNewSPI_fail_chipid(t *testing.T) { +func TestNewSPIBME280_fail_chipid(t *testing.T) { s := spitest.Playback{ Playback: conntest.Playback{ Ops: []conntest.IO{ @@ -161,7 +158,7 @@ func TestNewSPI_fail_chipid(t *testing.T) { } } -func TestNewI2C_fail_io(t *testing.T) { +func TestNewI2CBME280_fail_io(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -169,7 +166,7 @@ func TestNewI2C_fail_io(t *testing.T) { }, DontPanic: true, } - if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + if dev, err := NewI2C(&bus, 0x76, nil); dev != nil || err == nil { t.Fatal("read failed") } // The I/O didn't occur. @@ -179,7 +176,7 @@ func TestNewI2C_fail_io(t *testing.T) { } } -func TestNewI2C_fail_chipid(t *testing.T) { +func TestNewI2CBME280_fail_read_calib1(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -187,7 +184,7 @@ func TestNewI2C_fail_chipid(t *testing.T) { }, DontPanic: true, } - if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + if dev, err := NewI2C(&bus, 0x76, nil); dev != nil || err == nil { t.Fatal("invalid chip id") } if err := bus.Close(); err != nil { @@ -195,7 +192,7 @@ func TestNewI2C_fail_chipid(t *testing.T) { } } -func TestNewI2C_calib1(t *testing.T) { +func TestNewI2CBME280_read_calib2(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -209,8 +206,8 @@ func TestNewI2C_calib1(t *testing.T) { }, DontPanic: true, } - opts := Opts{Temperature: O1x, Address: 0} - if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { + opts := Opts{Temperature: O1x} + if dev, err := NewI2C(&bus, 0x76, &opts); dev != nil || err == nil { t.Fatal("2nd calib read failed") } if err := bus.Close(); err != nil { @@ -218,7 +215,7 @@ func TestNewI2C_calib1(t *testing.T) { } } -func TestNewI2C_calib2(t *testing.T) { +func TestNewI2C280_write_config(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -229,12 +226,12 @@ func TestNewI2C_calib2(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, }, DontPanic: true, } - if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + if dev, err := NewI2C(&bus, 0x76, nil); dev != nil || err == nil { t.Fatal("3rd calib read failed") } if err := bus.Close(); err != nil { @@ -242,10 +239,14 @@ func TestNewI2C_calib2(t *testing.T) { } } -func TestI2COpts_bad_addr(t *testing.T) { - bus := i2ctest.Playback{} - opts := Opts{Address: 1} - if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { +func TestNewI2C280Opts_temperature(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chip ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + }, + } + if dev, err := NewI2C(&bus, 0x76, &Opts{}); dev != nil || err == nil { t.Fatal("bad addr") } if err := bus.Close(); err != nil { @@ -253,18 +254,17 @@ func TestI2COpts_bad_addr(t *testing.T) { } } -func TestI2COpts(t *testing.T) { - bus := i2ctest.Playback{DontPanic: true} - opts := Opts{Address: 0x76} - if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { - t.Fatal("write fails") +func TestNewI2C280_bad_addr(t *testing.T) { + bus := i2ctest.Playback{} + if dev, err := NewI2C(&bus, 1, nil); dev != nil || err == nil { + t.Fatal("bad addr") } if err := bus.Close(); err != nil { t.Fatal(err) } } -func TestI2CSense_fail(t *testing.T) { +func TestI2CSenseBME280_fail(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -275,7 +275,7 @@ func TestI2CSense_fail(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -285,7 +285,7 @@ func TestI2CSense_fail(t *testing.T) { }, DontPanic: true, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -299,18 +299,69 @@ func TestI2CSense_fail(t *testing.T) { } } -func TestI2CSense_success(t *testing.T) { +func TestI2CSenseBMP280_success(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. - {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x58}}, // Calibration data. { Addr: 0x76, W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF4, 0x6d}}, + // Check if idle; not idle. + {Addr: 0x76, W: []byte{0xF3}, R: []byte{8}}, + // Check if idle. + {Addr: 0x76, W: []byte{0xF3}, R: []byte{0}}, + // Read. + {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0}}, + }, + } + dev, err := NewI2C(&bus, 0x76, nil) + if err != nil { + t.Fatal(err) + } + if s := dev.String(); s != "BMP280{playback(118)}" { + t.Fatal(s) + } + env := devices.Environment{} + if err := dev.Sense(&env); err != nil { + t.Fatal(err) + } + if env.Temperature != 23720 { + t.Fatalf("temp %d", env.Temperature) + } + if env.Pressure != 100943 { + t.Fatalf("pressure %d", env.Pressure) + } + if env.Humidity != 0 { + t.Fatalf("humidity %d", env.Humidity) + } + if err := dev.Halt(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestI2CSenseBME280_success(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chip ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -324,7 +375,7 @@ func TestI2CSense_success(t *testing.T) { {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, }, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -352,7 +403,7 @@ func TestI2CSense_success(t *testing.T) { } } -func TestI2CSense_idle_fail(t *testing.T) { +func TestI2CSense280_idle_fail(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -363,7 +414,7 @@ func TestI2CSense_idle_fail(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -373,7 +424,7 @@ func TestI2CSense_idle_fail(t *testing.T) { }, DontPanic: true, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -386,7 +437,7 @@ func TestI2CSense_idle_fail(t *testing.T) { } } -func TestI2CSense_command_fail(t *testing.T) { +func TestI2CSense280_command_fail(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -397,7 +448,7 @@ func TestI2CSense_command_fail(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -409,7 +460,7 @@ func TestI2CSense_command_fail(t *testing.T) { }, DontPanic: true, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -422,7 +473,7 @@ func TestI2CSense_command_fail(t *testing.T) { } } -func TestI2CSenseContinuous_success(t *testing.T) { +func TestI2CSenseContinuous280_success(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -433,7 +484,7 @@ func TestI2CSenseContinuous_success(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -453,7 +504,7 @@ func TestI2CSenseContinuous_success(t *testing.T) { {Addr: 0x76, W: []byte{0xF5, 0xa0, 0xf4, 0x6c}}, }, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -532,7 +583,7 @@ func TestI2CSenseContinuous_success(t *testing.T) { } } -func TestI2CSenseContinuous_command_fail(t *testing.T) { +func TestI2CSenseContinuous280_command_fail(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chip ID detection. @@ -543,7 +594,7 @@ func TestI2CSenseContinuous_command_fail(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -551,7 +602,7 @@ func TestI2CSenseContinuous_command_fail(t *testing.T) { }, DontPanic: true, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -560,7 +611,7 @@ func TestI2CSenseContinuous_command_fail(t *testing.T) { } } -func TestI2CSenseContinuous_sense_fail(t *testing.T) { +func TestI2CSenseContinuous280_sense_fail(t *testing.T) { if !testing.Verbose() { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) @@ -575,7 +626,7 @@ func TestI2CSenseContinuous_sense_fail(t *testing.T) { W: []byte{0x88}, R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, }, - // Calibration data. + // Calibration data humidity. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, @@ -585,7 +636,7 @@ func TestI2CSenseContinuous_sense_fail(t *testing.T) { }, DontPanic: true, } - dev, err := NewI2C(&bus, nil) + dev, err := NewI2C(&bus, 0x76, nil) if err != nil { t.Fatal(err) } @@ -603,16 +654,16 @@ func TestI2CSenseContinuous_sense_fail(t *testing.T) { } } -func TestCalibrationFloat(t *testing.T) { +func TestCalibration280Float(t *testing.T) { // Real data extracted from measurements from this device. tRaw := int32(524112) pRaw := int32(309104) hRaw := int32(30987) // Compare the values with the 3 algorithms. - temp, tFine := calib.compensateTempFloat(tRaw) - pres := calib.compensatePressureFloat(pRaw, tFine) - humi := calib.compensateHumidityFloat(hRaw, tFine) + temp, tFine := calib280.compensateTempFloat(tRaw) + pres := calib280.compensatePressureFloat(pRaw, tFine) + humi := calib280.compensateHumidityFloat(hRaw, tFine) if tFine != 117494 { t.Fatalf("tFine %d", tFine) } @@ -630,16 +681,16 @@ func TestCalibrationFloat(t *testing.T) { } } -func TestCalibrationInt(t *testing.T) { +func TestCalibration280Int(t *testing.T) { // Real data extracted from measurements from this device. tRaw := int32(524112) pRaw := int32(309104) hRaw := int32(30987) - temp, tFine := calib.compensateTempInt(tRaw) - pres64 := calib.compensatePressureInt64(pRaw, tFine) - pres32 := calib.compensatePressureInt32(pRaw, tFine) - humi := calib.compensateHumidityInt(hRaw, tFine) + temp, tFine := calib280.compensateTempInt(tRaw) + pres64 := calib280.compensatePressureInt64(pRaw, tFine) + pres32 := calib280.compensatePressureInt32(pRaw, tFine) + humi := calib280.compensateHumidityInt(hRaw, tFine) if tFine != 117407 { t.Fatalf("tFine %d", tFine) } @@ -665,14 +716,14 @@ func TestCalibrationInt(t *testing.T) { } } -func TestCalibration_limits_0(t *testing.T) { - c := calibration{h1: 0xFF, h2: 1, h3: 1, h6: 1} +func TestCalibration280_limits_0(t *testing.T) { + c := calibration280{h1: 0xFF, h2: 1, h3: 1, h6: 1} if v := c.compensateHumidityInt(0x7FFFFFFF>>14, 0xFFFFFFF); v != 0 { t.Fatal(v) } } -func TestCalibration_limits_419430400(t *testing.T) { +func TestCalibration280_limits_419430400(t *testing.T) { // TODO(maruel): Reverse the equation to overflow 419430400 } @@ -684,7 +735,7 @@ func Example() { log.Fatalf("failed to open I²C: %v", err) } defer bus.Close() - dev, err := NewI2C(bus, nil) + dev, err := NewI2C(bus, 0x76, nil) if err != nil { log.Fatalf("failed to initialize bme280: %v", err) } @@ -721,36 +772,51 @@ func TestOversampling(t *testing.T) { func TestStandby(t *testing.T) { data := []struct { - d time.Duration - s standby + isBME bool + d time.Duration + s standby }{ - {0, s500us}, - {time.Millisecond, s500us}, - {10 * time.Millisecond, s10ms}, - {20 * time.Millisecond, s20ms}, - {62500 * time.Microsecond, s62ms}, - {125 * time.Millisecond, s125ms}, - {250 * time.Millisecond, s250ms}, - {500 * time.Millisecond, s500ms}, - {time.Second, s1s}, - {time.Minute, s1s}, + {true, 0, s500us}, + {true, time.Millisecond, s500us}, + {true, 10 * time.Millisecond, s10msBME}, + {true, 20 * time.Millisecond, s20msBME}, + {true, 62500 * time.Microsecond, s62ms}, + {true, 125 * time.Millisecond, s125ms}, + {true, 250 * time.Millisecond, s250ms}, + {true, 500 * time.Millisecond, s500ms}, + {true, time.Second, s1s}, + {true, 2 * time.Second, s1s}, + {true, 4 * time.Second, s1s}, + {true, time.Minute, s1s}, + {false, 0, s500us}, + {false, time.Millisecond, s500us}, + {false, 10 * time.Millisecond, s62ms}, + {false, 20 * time.Millisecond, s62ms}, + {false, 62500 * time.Microsecond, s62ms}, + {false, 125 * time.Millisecond, s125ms}, + {false, 250 * time.Millisecond, s250ms}, + {false, 500 * time.Millisecond, s500ms}, + {false, time.Second, s1s}, + {false, 2 * time.Second, s2sBMP}, + {false, 4 * time.Second, s4sBMP}, + {false, time.Minute, s4sBMP}, } for i, line := range data { - if s := chooseStandby(line.d); s != line.s { + if s := chooseStandby(line.isBME, line.d); s != line.s { t.Fatalf("#%d chooseStandby(%s) = %d != %d", i, line.d, s, line.s) } } } -func TestCalibration_compensatePressureInt64(t *testing.T) { - c := calibration{} +func TestCalibration280_compensatePressureInt64(t *testing.T) { + c := calibration280{} if x := c.compensatePressureInt64(0, 0); x != 0 { t.Fatal(x) } } -func TestCalibration_compensateHumidityInt(t *testing.T) { - c := calibration{ +func TestCalibration280_compensateHumidityInt(t *testing.T) { + c := calibration280{ h1: 0xFF, } if x := c.compensateHumidityInt(0, 0); x != 0 { @@ -777,7 +843,7 @@ func floatEqual(a, b float32) bool { // raw has 20 bits of resolution. // // BUG(maruel): Output is incorrect. -func (c *calibration) compensatePressureInt32(raw, tFine int32) uint32 { +func (c *calibration280) compensatePressureInt32(raw, tFine int32) uint32 { x := tFine>>1 - 64000 y := (((x >> 2) * (x >> 2)) >> 11) * int32(c.p6) y += (x * int32(c.p5)) << 1 @@ -798,15 +864,13 @@ func (c *calibration) compensatePressureInt32(raw, tFine int32) uint32 { return uint32(int32(p) + ((x + y + int32(c.p7)) >> 4)) } -var _ devices.Environmental = &Dev{} - // Page 49 // compensateTempFloat returns temperature in °C. Output value of "51.23" // equals 51.23 °C. // // raw has 20 bits of resolution. -func (c *calibration) compensateTempFloat(raw int32) (float32, int32) { +func (c *calibration280) compensateTempFloat(raw int32) (float32, int32) { x := (float64(raw)/16384. - float64(c.t1)/1024.) * float64(c.t2) y := (float64(raw)/131072. - float64(c.t1)/8192.) * float64(c.t3) tFine := int32(x + y) @@ -817,7 +881,7 @@ func (c *calibration) compensateTempFloat(raw int32) (float32, int32) { // equals 96386.2 Pa = 963.862 hPa. // // raw has 20 bits of resolution. -func (c *calibration) compensatePressureFloat(raw, tFine int32) float32 { +func (c *calibration280) compensatePressureFloat(raw, tFine int32) float32 { x := float64(tFine)*0.5 - 64000. y := x * x * float64(c.p6) / 32768. y += x * float64(c.p5) * 2. @@ -838,7 +902,7 @@ func (c *calibration) compensatePressureFloat(raw, tFine int32) float32 { // represents 46.332 %rH. // // raw has 16 bits of resolution. -func (c *calibration) compensateHumidityFloat(raw, tFine int32) float32 { +func (c *calibration280) compensateHumidityFloat(raw, tFine int32) float32 { h := float64(tFine - 76800) h = (float64(raw) - float64(c.h4)*64. + float64(c.h5)/16384.*h) * float64(c.h2) / 65536. * (1. + float64(c.h6)/67108864.*h*(1.+float64(c.h3)/67108864.*h)) h *= 1. - float64(c.h1)*h/524288. diff --git a/devices/bmxx80/bmx280smoketest/README.md b/devices/bmxx80/bmx280smoketest/README.md new file mode 100644 index 0000000..fd3598c --- /dev/null +++ b/devices/bmxx80/bmx280smoketest/README.md @@ -0,0 +1,7 @@ +# 'bmx280' smoke test + +Verifies that two BME280/BMP280, one over I²C, one over SPI, can read roughly +the same temperature, humidity and pressure. + +It can also be leveraged to record the I/O to write playback unit tests. It is a +good example to reuse to write other device driver unit test. diff --git a/devices/bme280/bme280smoketest/bme280smoketest.go b/devices/bmxx80/bmx280smoketest/bmx280smoketest.go similarity index 83% rename from devices/bme280/bme280smoketest/bme280smoketest.go rename to devices/bmxx80/bmx280smoketest/bmx280smoketest.go index d4a22c9..f1769fb 100644 --- a/devices/bme280/bme280smoketest/bme280smoketest.go +++ b/devices/bmxx80/bmx280smoketest/bmx280smoketest.go @@ -2,10 +2,10 @@ // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -// Package bme280smoketest is leveraged by periph-smoketest to verify that two -// BME280, one over I²C, one over SPI, read roughly the same temperature, +// Package bmx280smoketest is leveraged by periph-smoketest to verify that two +// BME280/BMP280, one over I²C, one over SPI, read roughly the same temperature, // humidity and pressure. -package bme280smoketest +package bmx280smoketest import ( "flag" @@ -18,7 +18,7 @@ import ( "periph.io/x/periph/conn/spi/spireg" "periph.io/x/periph/conn/spi/spitest" "periph.io/x/periph/devices" - "periph.io/x/periph/devices/bme280" + "periph.io/x/periph/devices/bmxx80" ) // SmokeTest is imported by periph-smoketest. @@ -27,18 +27,19 @@ type SmokeTest struct { // Name implements the SmokeTest interface. func (s *SmokeTest) Name() string { - return "bme280" + return "bmx280" } // Description implements the SmokeTest interface. func (s *SmokeTest) Description() string { - return "Tests BME280 over I²C and SPI" + return "Tests BMx280 over I²C and SPI" } // Run implements the SmokeTest interface. func (s *SmokeTest) Run(args []string) (err error) { f := flag.NewFlagSet("buses", flag.ExitOnError) i2cID := f.String("i2c", "", "I²C bus to use") + i2cAddr := f.Uint("ia", 0x76, "I²C bus address to use; either 0x76 (BMx280, the default) or 0x77 (BMP180)") spiID := f.String("spi", "", "SPI port to use") record := f.Bool("r", false, "record operation (for playback unit testing)") f.Parse(args) @@ -63,12 +64,12 @@ func (s *SmokeTest) Run(args []string) (err error) { } }() if !*record { - return run(i2cBus, spiPort) + return run(i2cBus, uint16(*i2cAddr), spiPort) } i2cRecorder := i2ctest.Record{Bus: i2cBus} spiRecorder := spitest.Record{Port: spiPort} - err = run(&i2cRecorder, &spiRecorder) + err = run(&i2cRecorder, uint16(*i2cAddr), &spiRecorder) if len(i2cRecorder.Ops) != 0 { fmt.Printf("I²C recorder Addr: 0x%02X\n", i2cRecorder.Ops[0].Addr) } else { @@ -119,15 +120,15 @@ func (s *SmokeTest) Run(args []string) (err error) { return err } -func run(i2cBus i2c.Bus, spiPort spi.PortCloser) (err error) { - opts := &bme280.Opts{ - Temperature: bme280.O16x, - Pressure: bme280.O16x, - Humidity: bme280.O16x, - Filter: bme280.NoFilter, +func run(i2cBus i2c.Bus, i2cAddr uint16, spiPort spi.PortCloser) (err error) { + opts := &bmxx80.Opts{ + Temperature: bmxx80.O16x, + Pressure: bmxx80.O16x, + Humidity: bmxx80.O16x, + Filter: bmxx80.NoFilter, } - i2cDev, err2 := bme280.NewI2C(i2cBus, opts) + i2cDev, err2 := bmxx80.NewI2C(i2cBus, i2cAddr, opts) if err2 != nil { return err2 } @@ -137,7 +138,7 @@ func run(i2cBus i2c.Bus, spiPort spi.PortCloser) (err error) { } }() - spiDev, err2 := bme280.NewSPI(spiPort, opts) + spiDev, err2 := bmxx80.NewSPI(spiPort, opts) if err2 != nil { return err2 } diff --git a/devices/bmxx80/bmxx80.go b/devices/bmxx80/bmxx80.go new file mode 100644 index 0000000..7b05d72 --- /dev/null +++ b/devices/bmxx80/bmxx80.go @@ -0,0 +1,519 @@ +// Copyright 2016 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// Package bmxx80 controls a Bosch BMP180/BME280/BMP280 device over I²C, or SPI +// for the BMx280. +// +// BMx280 +// +// https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BME280_DS001-11.pdf +// +// https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMP280-DS001-18.pdf +// +// https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMP180-DS000-121.pdf +// +// The font the official datasheet on page 15 is hard to read, a copy with +// readable text can be found here: +// +// https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf +// +// Notes on the BMP180 datasheet +// +// The results of the calculations in the algorithm on page 15 are partly +// wrong. It looks like the original authors used non-integer calculations and +// some nubers were rounded. Take the results of the calculations with a grain +// of salt. +package bmxx80 + +import ( + "encoding/binary" + "errors" + "fmt" + "log" + "strings" + "sync" + "time" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/conn/spi" + "periph.io/x/periph/devices" +) + +// Oversampling affects how much time is taken to measure each of temperature, +// pressure and humidity. +// +// Using high oversampling and low standby results in highest power +// consumption, but this is still below 1mA so we generally don't care. +type Oversampling uint8 + +// Possible oversampling values. +// +// The higher the more time and power it takes to take a measurement. Even at +// 16x for all 3 sensors, it is less than 100ms albeit increased power +// consumption may increase the temperature reading. +const ( + Off Oversampling = 0 + O1x Oversampling = 1 + O2x Oversampling = 2 + O4x Oversampling = 3 + O8x Oversampling = 4 + O16x Oversampling = 5 +) + +const oversamplingName = "Off1x2x4x8x16x" + +var oversamplingIndex = [...]uint8{0, 3, 5, 7, 9, 11, 14} + +func (o Oversampling) String() string { + if o >= Oversampling(len(oversamplingIndex)-1) { + return fmt.Sprintf("Oversampling(%d)", o) + } + return oversamplingName[oversamplingIndex[o]:oversamplingIndex[o+1]] +} + +func (o Oversampling) asValue() int { + switch o { + case O1x: + return 1 + case O2x: + return 2 + case O4x: + return 4 + case O8x: + return 8 + case O16x: + return 16 + default: + return 0 + } +} + +func (o Oversampling) to180() uint8 { + switch o { + default: + fallthrough + case Off, O1x: + return 0 + case O2x: + return 1 + case O4x: + return 2 + case O8x, O16x: + return 3 + } +} + +// Filter specifies the internal IIR filter to get steadier measurements. +// +// Oversampling will get better measurements than filtering but at a larger +// power consumption cost, which may slightly affect temperature measurement. +type Filter uint8 + +// Possible filtering values. +// +// The higher the filter, the slower the value converges but the more stable +// the measurement is. +const ( + NoFilter Filter = 0 + F2 Filter = 1 + F4 Filter = 2 + F8 Filter = 3 + F16 Filter = 4 +) + +// Dev is a handle to an initialized BMxx80 device. +// +// The actual device type was auto detected. +type Dev struct { + d conn.Conn + isSPI bool + is280 bool + isBME bool + opts Opts + measDelay time.Duration + name string + os uint8 + cal180 calibration180 + cal280 calibration280 + + mu sync.Mutex + stop chan struct{} + wg sync.WaitGroup +} + +func (d *Dev) String() string { + // d.dev.Conn + return fmt.Sprintf("%s{%s}", d.name, d.d) +} + +// Sense requests a one time measurement as °C, kPa and % of relative humidity. +// +// The very first measurements may be of poor quality. +func (d *Dev) Sense(env *devices.Environment) error { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop != nil { + return d.wrap(errors.New("already sensing continuously")) + } + + if d.is280 { + err := d.writeCommands([]byte{ + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(forced), + }) + if err != nil { + return d.wrap(err) + } + time.Sleep(d.measDelay) + for idle := false; !idle; { + if idle, err = d.isIdle280(); err != nil { + return d.wrap(err) + } + } + return d.sense280(env) + } + return d.sense180(env) +} + +// SenseContinuous returns measurements as °C, kPa and % of relative humidity +// on a continuous basis. +// +// The application must call Halt() to stop the sensing when done to stop the +// sensor and close the channel. +// +// It's the responsibility of the caller to retrieve the values from the +// channel as fast as possible, otherwise the interval may not be respected. +func (d *Dev) SenseContinuous(interval time.Duration) (<-chan devices.Environment, error) { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop != nil { + // Don't send the stop command to the device. + close(d.stop) + d.stop = nil + d.wg.Wait() + } + + if d.is280 { + s := chooseStandby(d.isBME, interval-d.measDelay) + err := d.writeCommands([]byte{ + // config + 0xF5, byte(s)<<5 | byte(d.opts.Filter)<<2, + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(normal), + }) + if err != nil { + return nil, d.wrap(err) + } + } + + sensing := make(chan devices.Environment) + d.stop = make(chan struct{}) + d.wg.Add(1) + go func() { + defer d.wg.Done() + defer close(sensing) + d.sensingContinuous(interval, sensing, d.stop) + }() + return sensing, nil +} + +// Halt stops the BMxx80 from acquiring measurements as initiated by +// SenseContinuous(). +// +// It is recommended to call this function before terminating the process to +// reduce idle power usage and a goroutine leak. +func (d *Dev) Halt() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop == nil { + return nil + } + close(d.stop) + d.stop = nil + d.wg.Wait() + + if d.is280 { + // Page 27 (for register) and 12~13 section 3.3. + return d.writeCommands([]byte{ + // config + 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + }) + } + return nil +} + +// Opts is optional options to pass to the constructor. +// +// Recommended (and default) values are O4x for oversampling. +// +// Recommended sensing settings as per the datasheet: +// +// → Weather monitoring: manual sampling once per minute, all sensors O1x. +// Power consumption: 0.16µA, filter NoFilter. RMS noise: 3.3Pa / 30cm, 0.07%RH. +// +// → Humidity sensing: manual sampling once per second, pressure Off, humidity +// and temperature O1X, filter NoFilter. Power consumption: 2.9µA, 0.07%RH. +// +// → Indoor navigation: continuous sampling at 40ms with filter F16, pressure +// O16x, temperature O2x, humidity O1x, filter F16. Power consumption 633µA. +// RMS noise: 0.2Pa / 1.7cm. +// +// → Gaming: continuous sampling at 40ms with filter F16, pressure O4x, +// temperature O1x, humidity Off, filter F16. Power consumption 581µA. RMS +// noise: 0.3Pa / 2.5cm. +// +// See the datasheet for more details about the trade offs. +type Opts struct { + // Temperature can only be oversampled on BME280/BMP280. + // + // Temperature must be measured for pressure and humidity to be measured. + Temperature Oversampling + // Pressure can be oversampled up to 8x on BMP180 and 16x on BME280/BMP280. + Pressure Oversampling + // Humidity sensing is only supported on BME280. The value is ignored on other + // devices. + Humidity Oversampling + // Filter is only used while using SenseContinuous() and is only supported on + // BMx280. + Filter Filter +} + +func (o *Opts) delayTypical280() time.Duration { + // Page 51. + µs := 1000 + if o.Temperature != Off { + µs += 2000 * o.Temperature.asValue() + } + if o.Pressure != Off { + µs += 2000*o.Pressure.asValue() + 500 + } + if o.Humidity != Off { + µs += 2000*o.Humidity.asValue() + 500 + } + return time.Microsecond * time.Duration(µs) +} + +// NewI2C returns an object that communicates over I²C to BMP180/BME280/BMP280 +// environmental sensor. +// +// The address must be 0x76 or 0x77. BMP180 uses 0x77. BME280/BMP280 default to +// 0x76 and can optionally use 0x77. The value used depends on HW +// configuration of the sensor's SDO pin. +// +// It is recommended to call Halt() when done with the device so it stops +// sampling. +func NewI2C(b i2c.Bus, addr uint16, opts *Opts) (*Dev, error) { + switch addr { + case 0x76, 0x77: + default: + return nil, errors.New("bmxx80: given address not supported by device") + } + d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, isSPI: false} + if err := d.makeDev(opts); err != nil { + return nil, err + } + return d, nil +} + +// NewSPI returns an object that communicates over SPI to BME280 environmental +// sensor. +// +// It is recommended to call Halt() when done with the device so it stops +// sampling. +// +// When using SPI, the CS line must be used. +func NewSPI(p spi.Port, opts *Opts) (*Dev, error) { + // It works both in Mode0 and Mode3. + c, err := p.Connect(10000000, spi.Mode3, 8) + if err != nil { + return nil, fmt.Errorf("bmxx80: %v", err) + } + d := &Dev{d: c, isSPI: true} + if err := d.makeDev(opts); err != nil { + return nil, err + } + return d, nil +} + +// + +func (d *Dev) makeDev(opts *Opts) error { + if opts == nil { + opts = &defaults + } + d.opts = *opts + d.measDelay = d.opts.delayTypical280() + + // The device starts in 2ms as per datasheet. No need to wait for boot to be + // finished. + + var chipID [1]byte + // Read register 0xD0 to read the chip id. + if err := d.readReg(0xD0, chipID[:]); err != nil { + return err + } + switch chipID[0] { + case 0x55: + d.name = "BMP180" + d.os = opts.Pressure.to180() + case 0x58: + d.name = "BMP280" + d.is280 = true + d.opts.Humidity = Off + case 0x60: + d.name = "BME280" + d.is280 = true + d.isBME = true + default: + return fmt.Errorf("bmxx80: unexpected chip id %x", chipID[0]) + } + + if d.is280 && opts.Temperature == Off { + // Ignore the value for BMP180, since it's not controllable. + return d.wrap(errors.New("temperature measurement is required, use at least O1x")) + } + + if d.is280 { + // TODO(maruel): We may want to wait for isIdle280(). + // Read calibration data t1~3, p1~9, 8bits padding, h1. + var tph [0xA2 - 0x88]byte + if err := d.readReg(0x88, tph[:]); err != nil { + return err + } + // Read calibration data h2~6 + var h [0xE8 - 0xE1]byte + if d.isBME { + if err := d.readReg(0xE1, h[:]); err != nil { + return err + } + } + d.cal280 = newCalibration(tph[:], h[:]) + var b []byte + if d.isBME { + b = []byte{ + // ctrl_meas; put it to sleep otherwise the config update may be + // ignored. This is really just in case the device was somehow put + // into normal but was not Halt'ed. + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + // ctrl_hum + 0xF2, byte(d.opts.Humidity), + // config + 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, + // As per page 25, ctrl_meas must be re-written last. + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + } + } else { + // BMP280 doesn't have humidity to control. + b = []byte{ + // ctrl_meas; put it to sleep otherwise the config update may be + // ignored. This is really just in case the device was somehow put + // into normal but was not Halt'ed. + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + // config + 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, + // As per page 25, ctrl_meas must be re-written last. + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + } + } + if err := d.writeCommands(b); err != nil { + return err + } + return nil + } + // Read calibration data. + dev := mmr.Dev8{Conn: d.d, Order: binary.BigEndian} + if err := dev.ReadStruct(0xAA, &d.cal180); err != nil { + return d.wrap(err) + } + if !d.cal180.isValid() { + return d.wrap(errors.New("calibration data is invalid")) + } + return nil +} + +func (d *Dev) sensingContinuous(interval time.Duration, sensing chan<- devices.Environment, stop <-chan struct{}) { + t := time.NewTicker(interval) + defer t.Stop() + + var err error + for { + // Do one initial sensing right away. + var e devices.Environment + d.mu.Lock() + if d.is280 { + err = d.sense280(&e) + } else { + err = d.sense180(&e) + } + d.mu.Unlock() + if err != nil { + log.Printf("%s: failed to sense: %v", d, err) + return + } + select { + case sensing <- e: + case <-stop: + return + } + select { + case <-stop: + return + case <-t.C: + } + } +} + +func (d *Dev) readReg(reg uint8, b []byte) error { + // Page 32-33 + if d.isSPI { + // MSB is 0 for write and 1 for read. + read := make([]byte, len(b)+1) + write := make([]byte, len(read)) + // Rest of the write buffer is ignored. + write[0] = reg + if err := d.d.Tx(write, read); err != nil { + return d.wrap(err) + } + copy(b, read[1:]) + return nil + } + if err := d.d.Tx([]byte{reg}, b); err != nil { + return d.wrap(err) + } + return nil +} + +// writeCommands writes a command to the device. +// +// Warning: b may be modified! +func (d *Dev) writeCommands(b []byte) error { + if d.isSPI { + // Page 33; set RW bit 7 to 0. + for i := 0; i < len(b); i += 2 { + b[i] &^= 0x80 + } + } + if err := d.d.Tx(b, nil); err != nil { + return d.wrap(err) + } + return nil +} + +func (d *Dev) wrap(err error) error { + return fmt.Errorf("%s: %v", strings.ToLower(d.name), err) +} + +var defaults = Opts{ + Temperature: O4x, + Pressure: O4x, + Humidity: O4x, +} + +var _ devices.Environmental = &Dev{} +var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{}