From ec8e045dfdaacd3ff3ecb8939a91b6de627d00ca Mon Sep 17 00:00:00 2001 From: Toma Morisawa <13.35.104@gmail.com> Date: Thu, 11 Jun 2026 19:23:59 +0900 Subject: [PATCH 1/2] bmi270: add driver for BMI270 6-axis IMU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add I2C driver for the Bosch BMI270 inertial measurement unit, supporting configurable accelerometer (±2g/4g/8g/16g) and gyroscope (±125/250/500/1000/2000 dps) ranges. Includes: - Register definitions (registers.go) - Driver implementation with go:embed for config firmware (bmi270.go) - BMI270 config binary data (bmi270-config.bin) - Example usage (examples/bmi270/main.go) --- bmi270/bmi270-config.bin | Bin 0 -> 8192 bytes bmi270/bmi270.go | 272 +++++++++++++++++++++++++++++++++++++++ bmi270/registers.go | 21 +++ examples/bmi270/main.go | 48 +++++++ 4 files changed, 341 insertions(+) create mode 100644 bmi270/bmi270-config.bin create mode 100644 bmi270/bmi270.go create mode 100644 bmi270/registers.go create mode 100644 examples/bmi270/main.go diff --git a/bmi270/bmi270-config.bin b/bmi270/bmi270-config.bin new file mode 100644 index 0000000000000000000000000000000000000000..27dc7cce2e6acf3eda97d18b52b3fa8f7c4d35b8 GIT binary patch literal 8192 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 len(configBytes) { + end = len(configBytes) + } + + wordAddr := uint16(i / 2) + addrLow := byte(wordAddr & 0x0F) + addrHigh := byte(wordAddr >> 4) + d.wbuf[0] = reg_INIT_ADDR_0 + d.wbuf[1] = addrLow + d.wbuf[2] = addrHigh + if err := d.bus.Tx(uint16(d.address), d.wbuf[:3], nil); err != nil { + return err + } + + chunk := configBytes[i:end] + d.wbuf[0] = reg_INIT_DATA + copy(d.wbuf[1:], chunk) + if err := d.bus.Tx(uint16(d.address), d.wbuf[:1+len(chunk)], nil); err != nil { + return err + } + } + + if err := d.write1(reg_INIT_CTRL, 0x01); err != nil { + return err + } + time.Sleep(200 * time.Millisecond) + + start := time.Now() + for { + status, err := d.read1(reg_INTERNAL_STATUS) + if err != nil { + return err + } + if status == 0x01 { + break + } + if time.Since(start) >= 500*time.Millisecond { + return errInitFailed + } + time.Sleep(50 * time.Millisecond) + } + + if err := d.write1(reg_ACC_CONF, 0xA8); err != nil { + return err + } + + var rangeVal byte + switch d.accelRange { + case Accel2G: + rangeVal = 0x00 + case Accel4G: + rangeVal = 0x01 + case Accel8G: + rangeVal = 0x02 + case Accel16G: + rangeVal = 0x03 + default: + rangeVal = 0x00 + } + if err := d.write1(reg_ACC_RANGE, rangeVal); err != nil { + return err + } + + if err := d.write1(reg_GYR_CONF, 0xA8); err != nil { + return err + } + + var gyroRangeVal byte + switch d.gyroRange { + case Gyro2000DPS: + gyroRangeVal = 0x00 + case Gyro1000DPS: + gyroRangeVal = 0x01 + case Gyro500DPS: + gyroRangeVal = 0x02 + case Gyro250DPS: + gyroRangeVal = 0x03 + case Gyro125DPS: + gyroRangeVal = 0x04 + default: + gyroRangeVal = 0x00 + } + if err := d.write1(reg_GYR_RANGE, gyroRangeVal); err != nil { + return err + } + + if err := d.write1(reg_PWR_CTRL, 0x06); err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + + return nil +} + +// ReadAcceleration returns the acceleration in µg (micro-gravity). +// When one of the axes is pointing straight down and the sensor +// is not moving, the returned value will be around 1000000. +func (d *Device) ReadAcceleration() (x, y, z int32, err error) { + if err = d.bus.Tx(uint16(d.address), []byte{reg_ACC_DATA}, d.rbuf[:6]); err != nil { + return 0, 0, 0, err + } + + rawX := int16(uint16(d.rbuf[1])<<8 | uint16(d.rbuf[0])) + rawY := int16(uint16(d.rbuf[3])<<8 | uint16(d.rbuf[2])) + rawZ := int16(uint16(d.rbuf[5])<<8 | uint16(d.rbuf[4])) + + k := int32(61) + switch d.accelRange { + case Accel4G: + k = 122 + case Accel8G: + k = 244 + case Accel16G: + k = 488 + } + + x = int32(rawX) * k + y = int32(rawY) * k + z = int32(rawZ) * k + return +} + +// ReadRotation returns the angular velocity in µdps (micro-degrees/second). +func (d *Device) ReadRotation() (x, y, z int32, err error) { + if err = d.bus.Tx(uint16(d.address), []byte{reg_GYR_DATA}, d.rbuf[:6]); err != nil { + return 0, 0, 0, err + } + + rawX := int16(uint16(d.rbuf[1])<<8 | uint16(d.rbuf[0])) + rawY := int16(uint16(d.rbuf[3])<<8 | uint16(d.rbuf[2])) + rawZ := int16(uint16(d.rbuf[5])<<8 | uint16(d.rbuf[4])) + + k := int32(60976) + switch d.gyroRange { + case Gyro1000DPS: + k = 30488 + case Gyro500DPS: + k = 15244 + case Gyro250DPS: + k = 7622 + case Gyro125DPS: + k = 3811 + } + + x = int32(rawX) * k + y = int32(rawY) * k + z = int32(rawZ) * k + return +} + +func (d *Device) read1(register uint8) (uint8, error) { + d.wbuf[0] = register + err := d.bus.Tx(uint16(d.address), d.wbuf[:1], d.rbuf[:1]) + return d.rbuf[0], err +} + +func (d *Device) write1(register, val uint8) error { + d.wbuf[0] = register + d.wbuf[1] = val + return d.bus.Tx(uint16(d.address), d.wbuf[:2], nil) +} diff --git a/bmi270/registers.go b/bmi270/registers.go new file mode 100644 index 000000000..3f297f0a4 --- /dev/null +++ b/bmi270/registers.go @@ -0,0 +1,21 @@ +package bmi270 + +const ( + reg_CHIP_ID = 0x00 + reg_ACC_DATA = 0x0C + reg_GYR_DATA = 0x12 + reg_INTERNAL_STATUS = 0x21 + reg_ACC_CONF = 0x40 + reg_ACC_RANGE = 0x41 + reg_GYR_CONF = 0x42 + reg_GYR_RANGE = 0x43 + reg_INIT_CTRL = 0x59 + reg_INIT_ADDR_0 = 0x5B + reg_INIT_ADDR_1 = 0x5C + reg_INIT_DATA = 0x5E + reg_PWR_CONF = 0x7C + reg_PWR_CTRL = 0x7D + reg_CMD = 0x7E + + chipIDBMI270 = 0x24 +) diff --git a/examples/bmi270/main.go b/examples/bmi270/main.go new file mode 100644 index 000000000..163938fc0 --- /dev/null +++ b/examples/bmi270/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/bmi270" +) + +func main() { + time.Sleep(5 * time.Second) + + machine.I2C0.Configure(machine.I2CConfig{ + SCL: machine.SCL0_PIN, + SDA: machine.SDA0_PIN, + }) + + sensor := bmi270.NewI2C(machine.I2C0, bmi270.Address) + if !sensor.Connected() { + println("BMI270 not connected") + return + } + + cfg := bmi270.DefaultConfig() + if err := sensor.Configure(cfg); err != nil { + println("BMI270 configuration failed:", err.Error()) + return + } + + for { + time.Sleep(time.Second) + + accelX, accelY, accelZ, err := sensor.ReadAcceleration() + if err != nil { + println("Error reading acceleration:", err.Error()) + 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.Error()) + continue + } + fmt.Printf("Rotation: %.2f°/s %.2f°/s %.2f°/s\n", float32(gyroX)/1e6, float32(gyroY)/1e6, float32(gyroZ)/1e6) + } +} From da3922538bcee77d2d34305bc5791a451622a2fb Mon Sep 17 00:00:00 2001 From: Toma Morisawa <13.35.104@gmail.com> Date: Thu, 11 Jun 2026 19:28:34 +0900 Subject: [PATCH 2/2] bmi270: add smoketest entry --- smoketest.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/smoketest.sh b/smoketest.sh index 2990485cf..a7cf21ef0 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -17,6 +17,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bh1 tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/blinkm/main.go tinygo build -size short -o ./build/test.hex -target=pinetime ./examples/bma42x/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi160/main.go +tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/bmi270/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go