mirror of https://github.com/periph/devices
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
507 lines
12 KiB
Go
507 lines
12 KiB
Go
// 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 (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"periph.io/x/conn"
|
|
"periph.io/x/conn/i2c"
|
|
"periph.io/x/conn/mmr"
|
|
"periph.io/x/conn/physic"
|
|
"periph.io/x/conn/spi"
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
// DefaultOpts is the recommended default options.
|
|
var DefaultOpts = Opts{
|
|
Temperature: O4x,
|
|
Pressure: O4x,
|
|
Humidity: O4x,
|
|
}
|
|
|
|
// Opts defines the options for the device.
|
|
//
|
|
// 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 either a BME280 or
|
|
// BMP280 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(10*physic.MegaHertz, 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
|
|
}
|
|
|
|
// 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(e *physic.Env) 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)
|
|
}
|
|
doSleep(d.measDelay)
|
|
for idle := false; !idle; {
|
|
if idle, err = d.isIdle280(); err != nil {
|
|
return d.wrap(err)
|
|
}
|
|
}
|
|
return d.sense280(e)
|
|
}
|
|
return d.sense180(e)
|
|
}
|
|
|
|
// 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 physic.Env, 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 physic.Env)
|
|
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
|
|
}
|
|
|
|
// Precision implements physic.SenseEnv.
|
|
func (d *Dev) Precision(e *physic.Env) {
|
|
if d.is280 {
|
|
e.Temperature = 10 * physic.MilliKelvin
|
|
e.Pressure = 15625 * physic.MicroPascal / 4
|
|
} else {
|
|
e.Temperature = 100 * physic.MilliKelvin
|
|
e.Pressure = physic.Pascal
|
|
}
|
|
|
|
if d.isBME {
|
|
e.Humidity = 10000 / 1024 * physic.MicroRH
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
//
|
|
|
|
func (d *Dev) makeDev(opts *Opts) error {
|
|
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),
|
|
}
|
|
}
|
|
return d.writeCommands(b)
|
|
}
|
|
// 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<- physic.Env, stop <-chan struct{}) {
|
|
t := time.NewTicker(interval)
|
|
defer t.Stop()
|
|
|
|
var err error
|
|
for {
|
|
// Do one initial sensing right away.
|
|
e := physic.Env{}
|
|
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 doSleep = time.Sleep
|
|
|
|
var _ conn.Resource = &Dev{}
|
|
var _ physic.SenseEnv = &Dev{}
|