diff --git a/devices/bme280/bme280.go b/devices/bme280/bme280.go index 955362b..edf3724 100644 --- a/devices/bme280/bme280.go +++ b/devices/bme280/bme280.go @@ -11,6 +11,7 @@ package bme280 import ( "errors" + "fmt" "periph.io/x/periph/conn" "periph.io/x/periph/conn/i2c" @@ -232,7 +233,7 @@ func (d *Dev) makeDev(opts *Opts) error { return err } if chipID[0] != 0x60 { - return errors.New("bme280: unexpected chip id; is this a BME280?") + return fmt.Errorf("bme280: unexpected chip id %x; is this a BME280?", chipID[0]) } // Read calibration data t1~3, p1~9, 8bits padding, h1. var tph [0xA2 - 0x88]byte @@ -273,13 +274,16 @@ func (d *Dev) makeDev(opts *Opts) error { 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 err } - copy(b, read[:1]) + copy(b, read[1:]) + return nil } return d.d.Tx([]byte{reg}, b) } @@ -375,19 +379,6 @@ func (c *calibration) compensatePressureInt64(raw, tFine int32) uint32 { // raw has 16 bits of resolution. func (c *calibration) compensateHumidityInt(raw, tFine int32) uint32 { x := tFine - 76800 - /* - Yes, someone wrote the following in the datasheet unironically: - v_x1_u32r = (((((adc_H << 14) – (((BME280_S32_t)dig_H4) << 20) – - (((BME280_S32_t)dig_H5) * v_x1_u32r)) + ((BME280_S32_t)16384)) >> 15) * - (((((((v_x1_u32r * ((BME280_S32_t)dig_H6)) >> 10) * (((v_x1_u32r * - ((BME280_S32_t)dig_H3)) >> 11) + ((BME280_S32_t)32768))) >> 10) + - ((BME280_S32_t)2097152)) * ((BME280_S32_t)dig_H2) + 8192) >> 14)); - - v_x1_u32r = (v_x1_u32r – (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((BME280_S32_t)dig_H1)) >> 4)); - v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r); - v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r); - */ - // Here's a more "readable" version: x1 := raw<<14 - int32(c.h4)<<20 - int32(c.h5)*x x2 := (x1 + 16384) >> 15 x3 := (x * int32(c.h6)) >> 10 @@ -395,7 +386,6 @@ func (c *calibration) compensateHumidityInt(raw, tFine int32) uint32 { 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 diff --git a/devices/bme280/bme280_test.go b/devices/bme280/bme280_test.go index 61d4020..852840d 100644 --- a/devices/bme280/bme280_test.go +++ b/devices/bme280/bme280_test.go @@ -5,12 +5,16 @@ package bme280 import ( + "errors" "fmt" "log" "testing" + "periph.io/x/periph/conn/conntest" "periph.io/x/periph/conn/i2c/i2creg" "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/spi" + "periph.io/x/periph/conn/spi/spitest" "periph.io/x/periph/devices" ) @@ -36,7 +40,213 @@ var calib = calibration{ h6: 30, } -func TestRead(t *testing.T) { +func TestSPISense_success(t *testing.T) { + bus := spitest.Playback{ + Playback: conntest.Playback{ + Ops: []conntest.IO{ + // Chipd ID detection. + { + Write: []byte{0xD0, 0x00}, + Read: []byte{0x00, 0x60}, + }, + // Calibration data. + { + Write: []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}, + Read: []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. + { + Write: []byte{0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + Read: []byte{0x00, 0x5C, 0x01, 0x00, 0x15, 0x0F, 0x00, 0x1E}, + }, + {Write: []byte{0x74, 0xB4, 0x72, 0x05, 0x75, 0xA0, 0x74, 0xB7}}, + // Read. + { + Write: []byte{0xF7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + Read: []byte{0x00, 0x51, 0x9F, 0xC0, 0x9E, 0x3A, 0x50, 0x5E, 0x5B}, + }, + }, + }, + } + opts := Opts{ + Temperature: O16x, + Pressure: O16x, + Humidity: O16x, + Standby: S1s, + Filter: FOff, + } + dev, err := NewSPI(&bus, &opts) + if err != nil { + t.Fatal(err) + } + dev.Stop() + env := devices.Environment{} + if err := dev.Sense(&env); err != nil { + t.Fatal(err) + } + // TODO(maruel): The values do not make sense but I think I burned my SPI + // BME280 by misconnecting it in reverse for a few minutes. It still "work" + // but fail to read data. It could also be a bug in the driver. :( + if env.Temperature != 62680 { + t.Fatalf("temp %d", env.Temperature) + } + if env.Pressure != 99576 { + t.Fatalf("pressure %d", env.Pressure) + } + if env.Humidity != 995 { + t.Fatalf("humidity %d", env.Humidity) + } +} + +func TestNewSPI_fail(t *testing.T) { + if d, err := NewSPI(&spiFail{}, nil); d != nil || err == nil { + t.Fatal("DevParams() have failed") + } +} + +func TestNewSPI_fail_len(t *testing.T) { + bus := spitest.Playback{ + Playback: conntest.Playback{ + Ops: []conntest.IO{ + { + // Chipd ID detection. + Write: []byte{0xD0, 0x00}, + Read: []byte{0x00}, + }, + }, + }, + } + if dev, err := NewSPI(&bus, nil); dev != nil || err == nil { + t.Fatal("read failed") + } +} + +func TestNewSPI_fail_chipid(t *testing.T) { + bus := spitest.Playback{ + Playback: conntest.Playback{ + Ops: []conntest.IO{ + { + // Chipd ID detection. + Write: []byte{0xD0, 0x00}, + Read: []byte{0x00, 0xFF}, + }, + }, + }, + } + if dev, err := NewSPI(&bus, nil); dev != nil || err == nil { + t.Fatal("read failed") + } +} + +func TestNewI2C_fail(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, Write: []byte{0xd0}}, + }, + } + if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + t.Fatal("read failed") + } +} + +func TestNewI2C_chipid(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, Write: []byte{0xd0}, Read: []byte{0x60}}, + }, + } + if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + t.Fatal("invalid chip id") + } +} + +func TestNewI2C_calib1(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, Write: []byte{0xd0}, Read: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + Write: []byte{0x88}, + Read: []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}, + }, + }, + } + opts := Opts{Address: 0} + if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { + t.Fatal("2nd calib read failed") + } +} + +func TestNewI2C_calib2(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, Write: []byte{0xd0}, Read: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + Write: []byte{0x88}, + Read: []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. + {Addr: 0x76, Write: []byte{0xe1}, Read: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + }, + } + if dev, err := NewI2C(&bus, nil); dev != nil || err == nil { + t.Fatal("3rd calib read failed") + } +} + +func TestI2COpts_bad_addr(t *testing.T) { + bus := i2ctest.Playback{} + opts := Opts{Address: 1} + if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { + t.Fatal("bad addr") + } +} + +func TestI2COpts(t *testing.T) { + bus := i2ctest.Playback{} + opts := Opts{Address: 0x76} + if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { + t.Fatal("write fails") + } +} + +func TestI2CSense_fail(t *testing.T) { + // This data was generated with "bme280 -r" + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, Write: []byte{0xd0}, Read: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + Write: []byte{0x88}, + Read: []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. + {Addr: 0x76, Write: []byte{0xe1}, Read: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, Write: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xe0, 0xf4, 0x6f}, Read: nil}, + // Read. + {Addr: 0x76, Write: []byte{0xf7}}, + }, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + if dev.Sense(&devices.Environment{}) == nil { + t.Fatal("sense fail read") + } +} + +func TestI2CSense_success(t *testing.T) { // This data was generated with "bme280 -r" bus := i2ctest.Playback{ Ops: []i2ctest.IO{ @@ -54,6 +264,8 @@ func TestRead(t *testing.T) { {Addr: 0x76, Write: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xe0, 0xf4, 0x6f}, Read: nil}, // Read. {Addr: 0x76, Write: []byte{0xf7}, Read: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, + // Stop. + {Addr: 0x76, Write: []byte{0xf4, 0x0}}, }, } dev, err := NewI2C(&bus, nil) @@ -62,7 +274,7 @@ func TestRead(t *testing.T) { } env := devices.Environment{} if err := dev.Sense(&env); err != nil { - t.Fatalf("Sense(): %v", err) + t.Fatal(err) } if env.Temperature != 23720 { t.Fatalf("temp %d", env.Temperature) @@ -73,6 +285,12 @@ func TestRead(t *testing.T) { if env.Humidity != 6531 { t.Fatalf("humidity %d", env.Humidity) } + if err := dev.Stop(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } } func TestCalibrationFloat(t *testing.T) { @@ -137,6 +355,17 @@ func TestCalibrationInt(t *testing.T) { } } +func TestCalibration_limits_0(t *testing.T) { + c := calibration{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) { + // TODO(maruel): Reverse the equation to overflow 419430400 +} + // func Example() { @@ -156,6 +385,22 @@ func Example() { fmt.Printf("%8s %10s %9s\n", env.Temperature, env.Pressure, env.Humidity) } +func TestCalibration_compensatePressureInt64(t *testing.T) { + c := calibration{} + if x := c.compensatePressureInt64(0, 0); x != 0 { + t.Fatal(x) + } +} + +func TestCalibration_compensateHumidityInt(t *testing.T) { + c := calibration{ + h1: 0xFF, + } + if x := c.compensateHumidityInt(0, 0); x != 0 { + t.Fatal(x) + } +} + // var epsilon float32 = 0.00000001 @@ -248,3 +493,11 @@ func (c *calibration) compensateHumidityFloat(raw, tFine int32) float32 { } return float32(h) } + +type spiFail struct { + spitest.Playback +} + +func (s *spiFail) DevParams(maxHz int64, mode spi.Mode, bits int) error { + return errors.New("failing") +} diff --git a/devices/bme280/bme280smoketest/README.md b/devices/bme280/bme280smoketest/README.md new file mode 100644 index 0000000..ac83acb --- /dev/null +++ b/devices/bme280/bme280smoketest/README.md @@ -0,0 +1,7 @@ +# '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/bme280/bme280smoketest/bme280smoketest.go b/devices/bme280/bme280smoketest/bme280smoketest.go new file mode 100644 index 0000000..524788b --- /dev/null +++ b/devices/bme280/bme280smoketest/bme280smoketest.go @@ -0,0 +1,172 @@ +// 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 bme280smoketest is leveraged by periph-smoketest to verify that two +// BME280, one over I²C, one over SPI, read roughly the same temperature, +// humidity and pressure. +package bme280smoketest + +import ( + "flag" + "fmt" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/spi" + "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" +) + +// SmokeTest is imported by periph-smoketest. +type SmokeTest struct { +} + +// Name implements the SmokeTest interface. +func (s *SmokeTest) Name() string { + return "bme280" +} + +// Description implements the SmokeTest interface. +func (s *SmokeTest) Description() string { + return "Tests BME280 over I²C and SPI" +} + +// Run implements the SmokeTest interface. +func (s *SmokeTest) Run(args []string) (err error) { + f := flag.NewFlagSet("buses", flag.ExitOnError) + i2cName := f.String("i2c", "", "I²C bus to use") + spiName := f.String("spi", "", "SPI bus to use") + record := f.Bool("r", false, "record operation (for playback unit testing)") + f.Parse(args) + + i2cBus, err2 := i2creg.Open(*i2cName) + if err2 != nil { + return err2 + } + defer func() { + if err2 := i2cBus.Close(); err == nil { + err = err2 + } + }() + + spiBus, err2 := spireg.Open(*spiName) + if err2 != nil { + return err2 + } + defer func() { + if err2 := spiBus.Close(); err == nil { + err = err2 + } + }() + if !*record { + return run(i2cBus, spiBus) + } + + i2cRecorder := i2ctest.Record{Bus: i2cBus} + spiRecorder := spitest.Record{Conn: spiBus} + err = run(&i2cRecorder, &spiRecorder) + if len(i2cRecorder.Ops) != 0 { + fmt.Printf("I²C recorder Addr: 0x%02X\n", i2cRecorder.Ops[0].Addr) + } else { + fmt.Print("I²C recorder\n") + } + for _, op := range i2cRecorder.Ops { + fmt.Print(" Write: ") + for i, b := range op.Write { + if i != 0 { + fmt.Print(", ") + } + fmt.Printf("0x%02X", b) + } + fmt.Print("\n Read: ") + for i, b := range op.Read { + if i != 0 { + fmt.Print(", ") + } + fmt.Printf("0x%02X", b) + } + fmt.Print("\n") + } + fmt.Print("\nSPI recorder\n") + for _, op := range spiRecorder.Ops { + fmt.Print(" Write: ") + if len(op.Read) != 0 { + // Read data. + fmt.Printf("0x%02X\n Read: ", op.Write[0]) + // first byte is dummy. + for i, b := range op.Read[1:] { + if i != 0 { + fmt.Print(", ") + } + fmt.Printf("0x%02X", b) + } + } else { + // Write-only command. + for i, b := range op.Write { + if i != 0 { + fmt.Print(", ") + } + fmt.Printf("0x%02X", b) + } + fmt.Print("\n Read: ") + } + fmt.Print("\n") + } + return err +} + +func run(i2cBus i2c.Bus, spiBus spi.ConnCloser) (err error) { + opts := &bme280.Opts{ + Temperature: bme280.O16x, + Pressure: bme280.O16x, + Humidity: bme280.O16x, + Standby: bme280.S1s, + Filter: bme280.FOff, + } + + i2cDev, err2 := bme280.NewI2C(i2cBus, opts) + if err2 != nil { + return err2 + } + defer func() { + if err2 := i2cDev.Stop(); err == nil { + err = err2 + } + }() + + spiDev, err2 := bme280.NewSPI(spiBus, opts) + if err2 != nil { + return err2 + } + defer func() { + if err2 := spiDev.Stop(); err == nil { + err = err2 + } + }() + + // TODO(maruel): Generally the first measurement is way off. + i2cEnv := devices.Environment{} + spiEnv := devices.Environment{} + if err2 := i2cDev.Sense(&i2cEnv); err2 != nil { + return err2 + } + if err2 = spiDev.Sense(&spiEnv); err2 != nil { + return err2 + } + + // TODO(maruel): Determine acceptable threshold. + if d := i2cEnv.Temperature - spiEnv.Temperature; d > 1000 || d < -1000 { + return fmt.Errorf("Temperature delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Temperature, spiEnv.Temperature) + } + if d := i2cEnv.Pressure - spiEnv.Pressure; d > 100 || d < -100 { + return fmt.Errorf("Pressure delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Pressure, spiEnv.Pressure) + } + if d := i2cEnv.Humidity - spiEnv.Humidity; d > 100 || d < -100 { + return fmt.Errorf("Humidity delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Humidity, spiEnv.Humidity) + } + return nil +}