diff --git a/Makefile b/Makefile index f5edbe7..f8f8ada 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,8 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/blinkm/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi160/main.go + @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go diff --git a/README.md b/README.md index 87262a6..e4fdcae 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ The following 50 devices are supported. | [BH1750 ambient light sensor](https://www.mouser.com/ds/2/348/bh1750fvi-e-186247.pdf) | I2C | | [BlinkM RGB LED](http://thingm.com/fileadmin/thingm/downloads/BlinkM_datasheet.pdf) | I2C | | [BME280 humidity/pressure sensor](https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf) | I2C | +| [BMI160 accelerometer/gyroscope](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmi160-ds000.pdf) | SPI | | [BMP180 barometer](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf) | I2C | | [BMP280 temperature/barometer](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf) | I2C | | [Buzzer](https://en.wikipedia.org/wiki/Buzzer#Piezoelectric) | GPIO | diff --git a/bmi160/bmi160.go b/bmi160/bmi160.go new file mode 100644 index 0000000..75228d9 --- /dev/null +++ b/bmi160/bmi160.go @@ -0,0 +1,201 @@ +package bmi160 + +import "machine" + +import "time" + +// DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is +// also an I2C interface, but it is not yet supported. +type DeviceSPI struct { + // Chip select pin + CSB machine.Pin + + // SPI bus (requires chip select to be usable). + Bus machine.SPI +} + +// NewSPI returns a new device driver. The pin and SPI interface are not +// touched, provide a fully configured SPI object and call Configure to start +// using this device. +func NewSPI(csb machine.Pin, spi machine.SPI) *DeviceSPI { + return &DeviceSPI{ + CSB: csb, // chip select + Bus: spi, + } +} + +// Configure configures the BMI160 for use. It configures the CSB pin and +// configures the BMI160, but it does not configure the SPI interface (it is +// assumed to be up and running). +func (d *DeviceSPI) Configure() error { + d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.CSB.High() + + // The datasheet recommends doing a register read from address 0x7F to get + // SPI communication going: + // > If CSB sees a rising edge after power-up, the BMI160 interface switches + // > to SPI until a reset or the next power-up occurs. Therefore, a CSB + // > rising edge is needed before starting the SPI communication. Hence, it + // > is recommended to perform a SPI single read access to the ADDRESS 0x7F + // > before the actual communication in order to use the SPI interface. + d.readRegister(0x7F) + + // Power up the accelerometer. 0b0001_00nn is the command format, with 0b01 + // indicating normal mode. + d.runCommand(0b0001_0001) + + // Power up the gyroscope. 0b0001_01nn is the command format, with 0b01 + // indicating normal mode. + d.runCommand(0b0001_0101) + + // Wait until the device is fully initialized. Even after the command has + // finished, the gyroscope may not be fully powered on. Therefore, wait + // until we get an expected value. + // This takes 30ms or so. + for { + // Wait for the acc_pmu_status and gyr_pmu_status to both be 0b01. + if d.readRegister(reg_PMU_STATUS) == 0b0001_0100 { + break + } + } + + return nil +} + +// Connected check whether the device appears to be properly connected. It reads +// the CHIPID, which must be 0xD1 for the BMI160. +func (d *DeviceSPI) Connected() bool { + return d.readRegister(reg_CHIPID) == 0xD1 +} + +// Reset restores the device to the state after power up. This can be useful to +// easily disable the accelerometer and gyroscope to reduce current consumption. +func (d *DeviceSPI) Reset() error { + d.runCommand(0xB6) // softreset + return nil +} + +// ReadTemperature returns the temperature in celsius milli degrees (°C/1000). +func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) { + data := []byte{0x80 | reg_TEMPERATURE_0, 0, 0} + d.CSB.Low() + err = d.Bus.Tx(data, data) + d.CSB.High() + if err != nil { + return + } + rawTemperature := int16(uint16(data[1]) | uint16(data[2])<<8) + // 0x0000 is 23°C + // 0x7fff is ~87°C + // We use 0x8000 instead of 0x7fff to make the formula easier. The result + // should be near identical and shouldn't affect the result too much (the + // temperature sensor has an offset of around 2°C so isn't very reliable). + // So the formula is as follows: + // 1. Scale from 0x0000..0x8000 to 0..(87-23). + // rawTemperature * (87-23) / 0x8000 + // 2. Convert to centidegrees. + // rawTemperature * 1000 * (87-23) / 0x8000 + // 3. Add 23°C offset. + // rawTemperature * 1000 * (87-23) / 0x8000 + 23000 + // 4. Simplify. + // rawTemperature * 1000 * 64 / 0x8000 + 23000 + // rawTemperature * 64000 / 0x8000 + 23000 + // rawTemperature * 125 / 64 + 23000 + temperature = int32(rawTemperature)*125/64 + 23000 + return +} + +// ReadAcceleration reads the current acceleration from the device and returns +// it in µg (micro-gravity). When one of the axes is pointing straight to Earth +// and the sensor is not moving the returned value will be around 1000000 or +// -1000000. +func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) { + data := []byte{0x80 | reg_ACC_XL, 0, 0, 0, 0, 0, 0} + d.CSB.Low() + err = d.Bus.Tx(data, data) + d.CSB.High() + if err != nil { + return + } + // Now do two things: + // 1. merge the two values to a 16-bit number (and cast to a 32-bit integer) + // 2. scale the value to bring it in the -1000000..1000000 range. + // This is done with a trick. What we do here is essentially multiply by + // 1000000 and divide by 16384 to get the original scale, but to avoid + // overflow we do it at 1/64 of the value: + // 1000000 / 64 = 15625 + // 16384 / 64 = 256 + x = int32(int16(uint16(data[1])|uint16(data[2])<<8)) * 15625 / 256 + y = int32(int16(uint16(data[3])|uint16(data[4])<<8)) * 15625 / 256 + z = int32(int16(uint16(data[5])|uint16(data[6])<<8)) * 15625 / 256 + return +} + +// ReadRotation reads the current rotation from the device and returns it in +// µ°/s (micro-degrees/sec). This means that if you were to do a complete +// rotation along one axis and while doing so integrate all values over time, +// you would get a value close to 360000000. +func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) { + data := []byte{0x80 | reg_GYR_XL, 0, 0, 0, 0, 0, 0} + d.CSB.Low() + err = d.Bus.Tx(data, data) + d.CSB.High() + if err != nil { + return + } + // First the value is converted from a pair of bytes to a signed 16-bit + // value and then to a signed 32-bit value to avoid integer overflow. + // Then the value is scaled to µ°/s (micro-degrees per second). + // The default is 2000°/s full scale range for -32768..32767. + // The formula works as follows (taking X as an example): + // 1. Scale from 32768 to 2000. This means that it is in °/s units. + // rawX * 2000 / 32768 + // 2. Scale to µ°/s by multiplying by 1e6. + // rawX * 1e6 * 2000 / 32768 + // 3. Simplify. + // rawX * 2e9 / 32768 + // rawX * 1953125 / 32 + rawX := int32(int16(uint16(data[1]) | uint16(data[2])<<8)) + rawY := int32(int16(uint16(data[3]) | uint16(data[4])<<8)) + rawZ := int32(int16(uint16(data[5]) | uint16(data[6])<<8)) + x = int32(int64(rawX) * 1953125 / 32) + y = int32(int64(rawY) * 1953125 / 32) + z = int32(int64(rawZ) * 1953125 / 32) + return +} + +// runCommand runs a BMI160 command through the CMD register. It waits for the +// command to complete before returning. +func (d *DeviceSPI) runCommand(command uint8) { + d.writeRegister(reg_CMD, command) + for { + response := d.readRegister(reg_CMD) + if response == 0 { + return // command was completed + } + } +} + +// readRegister reads from a single BMI160 register. It should only be used for +// single register reads, not for reading multiple registers at once. +func (d *DeviceSPI) readRegister(address uint8) uint8 { + // I don't know why but it appears necessary to sleep for a bit here. + time.Sleep(time.Millisecond) + + data := []byte{0x80 | address, 0} + d.CSB.Low() + d.Bus.Tx(data, data) + d.CSB.High() + return data[1] +} + +// writeRegister writes a single byte BMI160 register. It should only be used +// for writing to a single register. +func (d *DeviceSPI) writeRegister(address, data uint8) { + // I don't know why but it appears necessary to sleep for a bit here. + time.Sleep(time.Millisecond) + + d.CSB.Low() + d.Bus.Tx([]byte{address, data}, []byte{0, 0}) + d.CSB.High() +} diff --git a/bmi160/registers.go b/bmi160/registers.go new file mode 100644 index 0000000..ad4a291 --- /dev/null +++ b/bmi160/registers.go @@ -0,0 +1,44 @@ +package bmi160 + +const ( + reg_CHIPID = 0x00 + reg_ERR_REG = 0x02 + reg_PMU_STATUS = 0x03 + reg_MAG_XL = 0x04 + reg_MAG_XH = 0x05 + reg_MAG_YL = 0x06 + reg_MAG_YH = 0x07 + reg_MAG_ZL = 0x08 + reg_MAG_ZH = 0x09 + reg_RHALL_L = 0x0A + reg_RHALL_H = 0x0B + reg_GYR_XL = 0x0C + reg_GYR_XH = 0x0D + reg_GYR_YL = 0x0E + reg_GYR_YH = 0x0F + reg_GYR_ZL = 0x10 + reg_GYR_ZH = 0x11 + reg_ACC_XL = 0x12 + reg_ACC_XH = 0x13 + reg_ACC_YL = 0x14 + reg_ACC_YH = 0x15 + reg_ACC_ZL = 0x16 + reg_ACC_ZH = 0x17 + reg_SENSORTIME_0 = 0x18 + reg_SENSORTIME_1 = 0x19 + reg_SENSORTIME_2 = 0x1A + reg_STATUS = 0x1B + reg_INT_STATUS_0 = 0x1C + reg_INT_STATUS_1 = 0x1D + reg_INT_STATUS_2 = 0x1E + reg_INT_STATUS_3 = 0x1F + reg_TEMPERATURE_0 = 0x20 + reg_TEMPERATURE_1 = 0x21 + reg_FIFO_LENGTH_0 = 0x22 + reg_FIFO_LENGTH_1 = 0x23 + reg_FIFO_DATA = 0x24 + + // ... + + reg_CMD = 0x7E +) diff --git a/examples/bmi160/main.go b/examples/bmi160/main.go new file mode 100644 index 0000000..364fef7 --- /dev/null +++ b/examples/bmi160/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "machine" + "time" + "tinygo.org/x/drivers/bmi160" +) + +func main() { + time.Sleep(5 * time.Second) + + machine.SPI0.Configure(machine.SPIConfig{}) + sensor := bmi160.NewSPI(machine.A5, machine.SPI0) + sensor.Configure() + + if !sensor.Connected() { + println("BMI160 not connected") + return + } + + for { + time.Sleep(time.Second) + + t, err := sensor.ReadTemperature() + if err != nil { + println("Error reading temperature", err) + continue + } + fmt.Printf("Temperature: %.2f °C\n", float32(t)/1000) + + accelX, accelY, accelZ, err := sensor.ReadAcceleration() + if err != nil { + println("Error reading acceleration", err) + continue + } + fmt.Printf("Acceleration: %.2fg %.2fg %.2fg\n", float32(accelX)/1e6, float32(accelY)/1e6, float32(accelZ)/1e6) + + gyroX, gyroY, gyroZ, err := sensor.ReadRotation() + if err != nil { + println("Error reading rotation", err) + continue + } + fmt.Printf("Rotation: %.2f°/s %.2f°/s %.2f°/s\n", float32(gyroX)/1e6, float32(gyroY)/1e6, float32(gyroZ)/1e6) + } +}