diff --git a/Makefile b/Makefile index 0dfa581..dd9cafc 100644 --- a/Makefile +++ b/Makefile @@ -41,5 +41,6 @@ smoke-test: tinygo build -size short -o ./build/test.elf -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.elf -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.elf -target=circuitplay-express ./examples/ws2812/main.go + tinygo build -size short -o ./build/test.elf -target=trinket-m0 ./examples/bme280/main.go test: clean fmt-check smoke-test diff --git a/bme280/bme280.go b/bme280/bme280.go new file mode 100644 index 0000000..00274cc --- /dev/null +++ b/bme280/bme280.go @@ -0,0 +1,251 @@ +package bme280 + +import ( + "machine" + "math" +) + +// calibrationCoefficients reads at startup and stores the calibration coefficients +type calibrationCoefficients struct { + t1 uint16 + t2 int16 + t3 int16 + p1 uint16 + p2 int16 + p3 int16 + p4 int16 + p5 int16 + p6 int16 + p7 int16 + p8 int16 + p9 int16 + h1 uint8 + h2 int16 + h3 uint8 + h4 int16 + h5 int16 + h6 int8 +} + +// Device wraps an I2C connection to a BME280 device. +type Device struct { + bus machine.I2C + Address uint16 + calibrationCoefficients calibrationCoefficients +} + +// New creates a new BME280 connection. The I2C bus must already be +// configured. +// +// This function only creates the Device object, it does not touch the device. +func New(bus machine.I2C) Device { + return Device{ + bus: bus, + Address: Address, + } +} + +// Configure sets up the device for communication and +// read the calibration coefficientes. +func (d *Device) Configure() { + + var data [24]byte + err := d.bus.ReadRegister(uint8(d.Address), REG_CALIBRATION, data[:]) + if err != nil { + return + } + + var h1 [1]byte + err = d.bus.ReadRegister(uint8(d.Address), REG_CALIBRATION_H1, h1[:]) + if err != nil { + return + } + + var h2lsb [7]byte + err = d.bus.ReadRegister(uint8(d.Address), REG_CALIBRATION_H2LSB, h2lsb[:]) + if err != nil { + return + } + + d.calibrationCoefficients.t1 = readUintLE(data[0], data[1]) + d.calibrationCoefficients.t2 = readIntLE(data[2], data[3]) + d.calibrationCoefficients.t3 = readIntLE(data[4], data[5]) + d.calibrationCoefficients.p1 = readUintLE(data[6], data[7]) + d.calibrationCoefficients.p2 = readIntLE(data[8], data[9]) + d.calibrationCoefficients.p3 = readIntLE(data[10], data[11]) + d.calibrationCoefficients.p4 = readIntLE(data[12], data[13]) + d.calibrationCoefficients.p5 = readIntLE(data[14], data[15]) + d.calibrationCoefficients.p6 = readIntLE(data[16], data[17]) + d.calibrationCoefficients.p7 = readIntLE(data[18], data[19]) + d.calibrationCoefficients.p8 = readIntLE(data[20], data[21]) + d.calibrationCoefficients.p9 = readIntLE(data[22], data[23]) + + d.calibrationCoefficients.h1 = h1[0] + d.calibrationCoefficients.h2 = readIntLE(h2lsb[0], h2lsb[1]) + d.calibrationCoefficients.h3 = h2lsb[2] + d.calibrationCoefficients.h6 = int8(h2lsb[6]) + d.calibrationCoefficients.h4 = 0 + (int16(h2lsb[3]) << 4) | (int16(h2lsb[4] & 0x0F)) + d.calibrationCoefficients.h5 = 0 + (int16(h2lsb[5]) << 4) | (int16(h2lsb[4]) >> 4) + + d.bus.WriteRegister(uint8(d.Address), CTRL_HUMIDITY_ADDR, []byte{0x3f}) + d.bus.WriteRegister(uint8(d.Address), CTRL_MEAS_ADDR, []byte{0xB7}) + d.bus.WriteRegister(uint8(d.Address), CTRL_CONFIG, []byte{0x00}) + +} + +// Connected returns whether a BME280 has been found. +// It does a "who am I" request and checks the response. +func (d *Device) Connected() bool { + data := []byte{0} + d.bus.ReadRegister(uint8(d.Address), WHO_AM_I, data) + return data[0] == CHIP_ID +} + +// Reset the device +func (d *Device) Reset() { + d.bus.WriteRegister(uint8(d.Address), CMD_RESET, []byte{0xB6}) +} + +// ReadTemperature returns the temperature in celsius milli degrees (ºC/1000) +func (d *Device) ReadTemperature() (int32, error) { + data, err := d.readData() + if err != nil { + return 0, err + } + + temp, _ := d.calculateTemp(data) + return temp, nil +} + +// ReadPressure returns the pressure in milli pascals mPa +func (d *Device) ReadPressure() (int32, error) { + data, err := d.readData() + if err != nil { + return 0, err + } + _, tFine := d.calculateTemp(data) + pressure := d.calculatePressure(data, tFine) + return pressure, nil +} + +// ReadHumidity returns the relative humidity in hundredths of a percent +func (d *Device) ReadHumidity() (int32, error) { + data, err := d.readData() + if err != nil { + return 0, err + } + _, tFine := d.calculateTemp(data) + humidity := d.calculateHumidity(data, tFine) + return humidity, nil +} + +// ReadAltitude returns the current altitude in meters based on the +// current barometric pressure and estimated pressure at sea level. +// Calculation is based on code from Adafruit BME280 library +// https://github.com/adafruit/Adafruit_BME280_Library +func (d *Device) ReadAltitude() (alt int32, err error) { + mPa, _ := d.ReadPressure() + atmP := float32(mPa) / 100000 + alt = int32(44330.0 * (1.0 - math.Pow(float64(atmP/SEALEVEL_PRESSURE), 0.1903))) + return +} + +// convert2Bytes converts two bytes to int32 +func convert2Bytes(msb byte, lsb byte) int32 { + return int32(readUint(msb, lsb)) +} + +// convert3Bytes converts three bytes to int32 +func convert3Bytes(msb byte, b1 byte, lsb byte) int32 { + return int32(((((uint32(msb) << 8) | uint32(b1)) << 8) | uint32(lsb)) >> 4) +} + +// readUint converts two bytes to uint16 +func readUint(msb byte, lsb byte) uint16 { + return (uint16(msb) << 8) | uint16(lsb) +} + +// readUintLE converts two little endian bytes to uint16 +func readUintLE(msb byte, lsb byte) uint16 { + temp := readUint(msb, lsb) + return (temp >> 8) | (temp << 8) +} + +// readIntLE converts two little endian bytes to int16 +func readIntLE(msb byte, lsb byte) int16 { + return int16(readUintLE(msb, lsb)) +} + +// readData does a burst read from 0xF7 to 0xF0 according to the datasheet +// resulting in an slice with 8 bytes 0-2 = pressure / 3-5 = temperature / 6-7 = humidity +func (d *Device) readData() (data [8]byte, err error) { + err = d.bus.ReadRegister(uint8(d.Address), REG_PRESSURE, data[:]) + if err != nil { + println(err) + return + } + return +} + +// calculateTemp uses the data slice and applies calibrations values on it to convert the value to milli degrees +// it also calculates the variable tFine which is used by the pressure and humidity calculation +func (d *Device) calculateTemp(data [8]byte) (int32, int32) { + + rawTemp := convert3Bytes(data[3], data[4], data[5]) + + var1 := (((rawTemp >> 3) - (int32(d.calibrationCoefficients.t1) << 1)) * int32(d.calibrationCoefficients.t2)) >> 11 + var2 := (((((rawTemp >> 4) - int32(d.calibrationCoefficients.t1)) * ((rawTemp >> 4) - int32(d.calibrationCoefficients.t1))) >> 12) * int32(d.calibrationCoefficients.t3)) >> 14 + + tFine := var1 + var2 + T := (tFine*5 + 128) >> 8 + return (10 * T), tFine +} + +// calculatePressure uses the data slice and applies calibrations values on it to convert the value to milli pascals mPa +func (d *Device) calculatePressure(data [8]byte, tFine int32) int32 { + + rawPressure := convert3Bytes(data[0], data[1], data[2]) + + var1 := int64(tFine) - 128000 + var2 := var1 * var1 * int64(d.calibrationCoefficients.p6) + var2 = var2 + ((var1 * int64(d.calibrationCoefficients.p5)) << 17) + var2 = var2 + (int64(d.calibrationCoefficients.p4) << 35) + var1 = ((var1 * var1 * int64(d.calibrationCoefficients.p3)) >> 8) + ((var1 * int64(d.calibrationCoefficients.p2)) << 12) + var1 = ((int64(1) << 47) + var1) * int64(d.calibrationCoefficients.p1) >> 33 + + if var1 == 0 { + return 0 // avoid exception caused by division by zero + } + p := int64(1048576 - rawPressure) + p = (((p << 31) - var2) * 3125) / var1 + var1 = (int64(d.calibrationCoefficients.p9) * (p >> 13) * (p >> 13)) >> 25 + var2 = (int64(d.calibrationCoefficients.p8) * p) >> 19 + + p = ((p + var1 + var2) >> 8) + (int64(d.calibrationCoefficients.p7) << 4) + p = (p / 256) + return int32(1000 * p) +} + +// calculateHumidity uses the data slice and applies calibrations values on it to convert the value to relative humidity in hundredths of a percent +func (d *Device) calculateHumidity(data [8]byte, tFine int32) int32 { + + rawHumidity := convert2Bytes(data[6], data[7]) + + h := float32(tFine) - 76800 + + if h == 0 { + println("invalid value") + } + + var1 := float32(rawHumidity) - (float32(d.calibrationCoefficients.h4)*64.0 + + (float32(d.calibrationCoefficients.h5) / 16384.0 * h)) + + var2 := float32(d.calibrationCoefficients.h2) / 65536.0 * + (1.0 + float32(d.calibrationCoefficients.h6)/67108864.0*h* + (1.0+float32(d.calibrationCoefficients.h3)/67108864.0*h)) + + h = var1 * var2 + h = h * (1 - float32(d.calibrationCoefficients.h1)*h/524288) + return int32(100 * h) + +} diff --git a/bme280/registers.go b/bme280/registers.go new file mode 100644 index 0000000..34c9cbd --- /dev/null +++ b/bme280/registers.go @@ -0,0 +1,25 @@ +package bme280 + +// Constants/addresses used for I2C. + +// The I2C address which this device listens to. +const Address = 0x76 + +// Registers. Names, addresses and comments copied from the datasheet. +const ( + CTRL_MEAS_ADDR = 0xF4 + CTRL_HUMIDITY_ADDR = 0xF2 + CTRL_CONFIG = 0xF5 + REG_PRESSURE = 0xF7 + REG_CALIBRATION = 0x88 + REG_CALIBRATION_H1 = 0xA1 + REG_CALIBRATION_H2LSB = 0xE1 + CMD_RESET = 0xE0 + + WHO_AM_I = 0xD0 + CHIP_ID = 0x60 +) + +const ( + SEALEVEL_PRESSURE float32 = 1013.25 // in hPa +) diff --git a/examples/bme280/main.go b/examples/bme280/main.go new file mode 100644 index 0000000..c6d8a06 --- /dev/null +++ b/examples/bme280/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers/bme280" +) + +func main() { + + machine.I2C0.Configure(machine.I2CConfig{}) + sensor := bme280.New(machine.I2C0) + sensor.Configure() + + connected := sensor.Connected() + if !connected { + println("BME280 not detected") + } + println("BME280 detected") + + for { + temp, _ := sensor.ReadTemperature() + println("Temperature:", strconv.FormatFloat(float64(temp)/1000, 'f', 2, 64), "ºC") + press, _ := sensor.ReadPressure() + println("Pressure:", strconv.FormatFloat(float64(press)/100000, 'f', 2, 64), "hPa") + hum, _ := sensor.ReadHumidity() + println("Humidity:", strconv.FormatFloat(float64(hum)/100, 'f', 2, 64), "%") + alt, _ := sensor.ReadAltitude() + println("Altitude:", alt, "m") + + time.Sleep(2 * time.Second) + } +}