diff --git a/Makefile b/Makefile index 1b36feb..dc0b3e8 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,8 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go + @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go diff --git a/bmp388/bmp388.go b/bmp388/bmp388.go new file mode 100644 index 0000000..27b5cf6 --- /dev/null +++ b/bmp388/bmp388.go @@ -0,0 +1,249 @@ +package bmp388 + +import ( + "errors" + + "tinygo.org/x/drivers" +) + +var ( + errConfigWrite = errors.New("bmp388: failed to configure sensor, check connection") + errConfig = errors.New("bmp388: there is a problem with the configuration, try reducing ODR") + errCaliRead = errors.New("bmp388: failed to read calibration coefficient register") + errSoftReset = errors.New("bmp388: failed to perform a soft reset") + errNotConnected = errors.New("bmp388: not connected") +) + +type Oversampling byte +type Mode byte +type OutputDataRate byte +type FilterCoefficient byte + +// Config contains settings for filtering, sampling, and modes of operation +type Config struct { + Pressure Oversampling + Temperature Oversampling + Mode Mode + ODR OutputDataRate + IIR FilterCoefficient +} + +// Device wraps the I2C connection and configuration values for the BMP388 +type Device struct { + bus drivers.I2C + Address uint8 + cali calibrationCoefficients + Config Config +} + +type calibrationCoefficients struct { + // Temperature compensation + t1 uint16 + t2 uint16 + t3 int8 + + // Pressure compensation + p1 int16 + p2 int16 + p3 int8 + p4 int8 + p5 uint16 + p6 uint16 + p7 int8 + p8 int8 + p9 int16 + p10 int8 + p11 int8 +} + +// New returns a bmp388 struct with the default I2C address. Configure must also be called after instanting +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: Address, + } +} + +// Configure can enable settings on the BMP388 and reads the calibration coefficients +func (d *Device) Configure(config Config) (err error) { + d.Config = config + + if d.Config == (Config{}) { + d.Config.Mode = Normal + } + + // Turning on the pressure and temperature sensors and setting the measurement mode + err = d.writeRegister(RegPwrCtrl, PwrPress|PwrTemp|byte(d.Config.Mode)) + + // Configure the oversampling, output data rate, and iir filter coefficient settings + err = d.writeRegister(RegOSR, byte(d.Config.Pressure|d.Config.Temperature<<3)) + err = d.writeRegister(RegODR, byte(d.Config.ODR)) + err = d.writeRegister(RegIIR, byte(d.Config.IIR<<1)) + + if err != nil { + return errConfigWrite + } + + // Check if there is a problem with the given configuration + if d.configurationError() { + return errConfig + } + + // Reading the builtin calibration coefficients and parsing them per the datasheet. The compensation formula given + // in the datasheet is implemented in floating point + buffer, err := d.readRegister(RegCali, 21) + if err != nil { + return errCaliRead + } + + d.cali.t1 = uint16(buffer[1])<<8 | uint16(buffer[0]) + d.cali.t2 = uint16(buffer[3])<<8 | uint16(buffer[2]) + d.cali.t3 = int8(buffer[4]) + + d.cali.p1 = int16(buffer[6])<<8 | int16(buffer[5]) + d.cali.p2 = int16(buffer[8])<<8 | int16(buffer[7]) + d.cali.p3 = int8(buffer[9]) + d.cali.p4 = int8(buffer[10]) + d.cali.p5 = uint16(buffer[12])<<8 | uint16(buffer[11]) + d.cali.p6 = uint16(buffer[14])<<8 | uint16(buffer[13]) + d.cali.p7 = int8(buffer[15]) + d.cali.p8 = int8(buffer[16]) + d.cali.p9 = int16(buffer[18])<<8 | int16(buffer[17]) + d.cali.p10 = int8(buffer[19]) + d.cali.p11 = int8(buffer[20]) + + return nil +} + +// Read the temperature registers and compute a compensation value for the temperature and pressure compensation +// calculations. This is not the temperature itself. +func (d *Device) tlinCompensate() (int64, error) { + rawTemp, err := d.readSensorData(RegTemp) + if err != nil { + return 0, err + } + + // pulled from C driver: https://github.com/BoschSensortec/BMP3-Sensor-API/blob/master/bmp3.c + partialData1 := rawTemp - (256 * int64(d.cali.t1)) + partialData2 := int64(d.cali.t2) * partialData1 + partialData3 := (partialData1 * partialData1) + partialData4 := partialData3 * int64(d.cali.t3) + partialData5 := (partialData2 * 262144) + partialData4 + return partialData5 / 4294967296, nil + +} + +// ReadTemperature returns the temperature in centicelsius, i.e 2426 / 100 = 24.26 C +func (d *Device) ReadTemperature() (int32, error) { + + tlin, err := d.tlinCompensate() + if err != nil { + return 0, err + } + + temp := (tlin * 25) / 16384 + return int32(temp), nil +} + +// ReadPressure returns the pressure in centipascals, i.e 10132520 / 100 = 101325.20 Pa +func (d *Device) ReadPressure() (int32, error) { + + tlin, err := d.tlinCompensate() + if err != nil { + return 0, err + } + rawPress, err := d.readSensorData(RegPress) + if err != nil { + return 0, err + } + + // code pulled from bmp388 C driver: https://github.com/BoschSensortec/BMP3-Sensor-API/blob/master/bmp3.c + partialData1 := tlin * tlin + partialData2 := partialData1 / 64 + partialData3 := (partialData2 * tlin) / 256 + partialData4 := (int64(d.cali.p8) * partialData3) / 32 + partialData5 := (int64(d.cali.p7) * partialData1) * 16 + partialData6 := (int64(d.cali.p6) * tlin) * 4194304 + offset := (int64(d.cali.p5) * 140737488355328) + partialData4 + partialData5 + partialData6 + partialData2 = (int64(d.cali.p4) * partialData3) / 32 + partialData4 = (int64(d.cali.p3) * partialData1) * 4 + partialData5 = (int64(d.cali.p2) - 16384) * tlin * 2097152 + sensitivity := ((int64(d.cali.p1) - 16384) * 70368744177664) + partialData2 + partialData4 + partialData5 + partialData1 = (sensitivity / 16777216) * rawPress + partialData2 = int64(d.cali.p10) * tlin + partialData3 = partialData2 + (65536 * int64(d.cali.p9)) + partialData4 = (partialData3 * rawPress) / 8192 + + // dividing by 10 followed by multiplying by 10 + // To avoid overflow caused by (pressure * partial_data4) + partialData5 = (rawPress * (partialData4 / 10)) / 512 + partialData5 = partialData5 * 10 + partialData6 = (int64)(uint64(rawPress) * uint64(rawPress)) + partialData2 = (int64(d.cali.p11) * partialData6) / 65536 + partialData3 = (partialData2 * rawPress) / 128 + partialData4 = (offset / 4) + partialData1 + partialData5 + partialData3 + compPress := ((uint64(partialData4) * 25) / uint64(1099511627776)) + return int32(compPress), nil +} + +// SoftReset commands the BMP388 to reset of all user configuration settings +func (d *Device) SoftReset() error { + err := d.writeRegister(RegCmd, SoftReset) + if err != nil { + return errSoftReset + } + return nil +} + +// Connected tries to reach the bmp388 and check its chip id register. Returns true if it was able to successfully +// communicate over i2c and returns the correct value +func (d *Device) Connected() bool { + data, err := d.readRegister(RegChipId, 1) + return err == nil && data[0] == ChipId // returns true if i2c comm was good and response equals 0x50 +} + +// SetMode changes the run mode of the sensor, NORMAL is the one to use for most cases. Use FORCED if you plan to take +// measurements infrequently and want to conserve power. SLEEP will of course put the sensor to sleep +func (d *Device) SetMode(mode Mode) error { + d.Config.Mode = mode + return d.writeRegister(RegPwrCtrl, PwrPress|PwrTemp|byte(d.Config.Mode)) +} + +func (d *Device) readSensorData(register byte) (data int64, err error) { + + if !d.Connected() { + return 0, errNotConnected + } + + // put the sensor back into forced mode to get a reading, the sensor goes back to sleep after taking one read in + // forced mode + if d.Config.Mode != Normal { + err = d.SetMode(Forced) + if err != nil { + return + } + } + + bytes, err := d.readRegister(register, 3) + if err != nil { + return + } + data = int64(bytes[2])<<16 | int64(bytes[1])<<8 | int64(bytes[0]) + return +} + +// configurationError checks the register error for the configuration error bit. The bit is cleared on read by the bmp. +func (d *Device) configurationError() bool { + data, err := d.readRegister(RegErr, 1) + return err == nil && (data[0]&0x04) != 0 +} + +func (d *Device) readRegister(register byte, len int) (data []byte, err error) { + data = make([]byte, len) + err = d.bus.ReadRegister(d.Address, register, data) + return +} + +func (d *Device) writeRegister(register byte, data byte) error { + return d.bus.WriteRegister(d.Address, register, []byte{data}) +} diff --git a/bmp388/registers.go b/bmp388/registers.go new file mode 100644 index 0000000..5cd4fde --- /dev/null +++ b/bmp388/registers.go @@ -0,0 +1,84 @@ +// Package bmp388 provides a driver for Bosch's BMP388 digital temperature & pressure sensor. +// The datasheet can be found here: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp388-ds001.pdf +package bmp388 + +const Address byte = 0x77 // default I2C address + +const ( + RegChipId byte = 0x00 // useful for checking the connection + RegCali byte = 0x31 // pressure & temperature compensation calibration coefficients + RegPress byte = 0x04 // start of pressure data registers + RegTemp byte = 0x07 // start of temperature data registers + RegPwrCtrl byte = 0x1B // measurement mode & pressure/temperature sensor power register + RegOSR byte = 0x1C // oversampling settings register + RegODR byte = 0x1D // + RegCmd byte = 0x7E // miscellaneous command register + RegStat byte = 0x03 // sensor status register + RegErr byte = 0x02 // error status register + RegIIR byte = 0x1F +) + +const ( + ChipId byte = 0x50 // correct response if reading from chip id register + PwrPress byte = 0x01 // power on pressure sensor + PwrTemp byte = 0x02 // power on temperature sensor + SoftReset byte = 0xB6 // command to reset all user configuration + DRDYPress byte = 0x20 // for checking if pressure data is ready + DRDYTemp byte = 0x40 // for checking if pressure data is ready +) + +// The difference between forced and normal mode is the bmp388 goes to sleep after taking a measurement in forced mode. +// Set it to forced if you intend to take measurements sporadically and want to save power. The driver will handle +// waking the sensor up when the sensor is in forced mode. +const ( + Normal Mode = 0x30 + Forced Mode = 0x16 + Sleep Mode = 0x00 +) + +// Increasing sampling rate increases precision but also the wait time for measurements. The datasheet has a table of +// suggested values for oversampling, output data rates, and iir filter coefficients by use case. +const ( + Sampling1X Oversampling = iota + Sampling2X + Sampling4X + Sampling8X + Sampling16X + Sampling32X +) + +// Output data rates in Hz. If increasing the sampling rates you need to decrease the output data rates, else the bmp388 +// will freeze and Configure() will return a configuration error message. In that case keep decreasing the data rate +// until the bmp is happy +const ( + Odr200 OutputDataRate = iota + Odr100 + Odr50 + Odr25 + Odr12p5 + Odr6p25 + Odr3p1 + Odr1p5 + Odr0p78 + Odr0p39 + Odr0p2 + Odr0p1 + Odr0p05 + Odr0p02 + Odr0p01 + Odr0p006 + Odr0p003 + Odr0p0015 +) + +// IIR filter coefficients, higher values means steadier measurements but slower reaction times +const ( + Coeff0 FilterCoefficient = iota + Coeff1 + Coeff3 + Coeff7 + Coeff15 + Coeff31 + Coeff63 + Coeff127 +) diff --git a/examples/bmp388/main.go b/examples/bmp388/main.go new file mode 100644 index 0000000..06bc9e2 --- /dev/null +++ b/examples/bmp388/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers/bmp388" +) + +func main() { + + machine.I2C0.Configure(machine.I2CConfig{}) + + sensor := bmp388.New(machine.I2C0) + if !sensor.Connected() { + println("Uh oh, BMP388 not detected") + return + } + + // The accuracy of the sensor can be increased, at the cost of a slower output rate. Table 9 in Section 3.5 of the + // datasheet has recommended settings for common use cases. If increasing the sampling rate, the output data rate + // (ODR) will likely have to be decreased. Configure() will return an error if there's a problem with the + // configuration settings - keep decreasing the ODR and cycling the power to the sensor until it is happy. + err := sensor.Configure(bmp388.Config{ + Pressure: bmp388.Sampling8X, + Temperature: bmp388.Sampling2X, + ODR: bmp388.Odr25, + IIR: bmp388.Coeff0, + Mode: bmp388.Normal, + }) + + // This is also fine + // err := sensor.Configure(bmp388.BMP388Config{}) + + if err != nil { + println(err) + } + + for { + temp, err := sensor.ReadTemperature() // returns the temperature in centicelsius + press, err := sensor.ReadPressure() // returns the pressure in centipascals + + if err != nil { + println(err) + } else { + println("Temperature: " + strconv.FormatInt(int64(temp), 10) + " cC") + println("Pressure: " + strconv.FormatInt(int64(press), 10) + " cPa\n") + } + + time.Sleep(time.Second) + } +}