mirror of https://github.com/periph/devices
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 #155pull/1/head
parent
bbed3b44f7
commit
5e39729535
@ -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{}
|
|
||||||
@ -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.
|
|
||||||
@ -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{}
|
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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.
|
||||||
@ -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{}
|
||||||
Loading…
Reference in New Issue