diff --git a/bma42x/bma421-config-waspos.bin b/bma42x/bma421-config-waspos.bin new file mode 100644 index 000000000..cc49293f4 Binary files /dev/null and b/bma42x/bma421-config-waspos.bin differ diff --git a/bma42x/bma425-config-waspos.bin b/bma42x/bma425-config-waspos.bin new file mode 100644 index 000000000..886fa2ef4 Binary files /dev/null and b/bma42x/bma425-config-waspos.bin differ diff --git a/bma42x/bma42x.go b/bma42x/bma42x.go new file mode 100644 index 000000000..46b5e9108 --- /dev/null +++ b/bma42x/bma42x.go @@ -0,0 +1,352 @@ +// Package bma42x provides a driver for the BMA421 and BMA425 accelerometer +// chips. +// +// Here is a reasonably good datasheet: +// https://datasheet.lcsc.com/lcsc/1912111437_Bosch-Sensortec-BMA425_C437656.pdf +// +// This driver was originally written for the PineTime, using the datasheet as a +// guide. There is an open source C driver provided by Bosch, but unfortunately +// it needs some small modifications to work with other chips (most importantly, +// the "config file"). +// The InfiniTime and Wasp-OS drivers for this accelerometer have also been used +// to figure out some driver details (especially step counting). +package bma42x + +import ( + _ "embed" + "errors" + "reflect" + "time" + "unsafe" + + "tinygo.org/x/drivers" +) + +// Driver for BMA421 and BMA425: +// BMA421: https://files.pine64.org/doc/datasheet/pinetime/BST-BMA421-FL000.pdf +// BMA425: https://datasheet.lcsc.com/lcsc/1912111437_Bosch-Sensortec-BMA425_C437656.pdf + +// This is the BMA421 firmware from the Wasp-OS project. +// It is identical to the so-called BMA423 firmware in InfiniTime, which I +// suspect to be actually a BMA421 firmware. I don't know where this firmware +// comes from or what the licensing status is. +// It has the FEATURES_IN command prepended, so that it can be written directly +// using I2C.Tx. +// Source: https://github.com/wasp-os/bma42x-upy/blob/master/BMA42X-Sensor-API/bma421.h +// +//go:embed bma421-config-waspos.bin +var bma421Firmware string + +// Same as the BMA421 firmware, but for the BMA425. +// Source: https://github.com/wasp-os/bma42x-upy/blob/master/BMA42X-Sensor-API/bma425.h +// +//go:embed bma425-config-waspos.bin +var bma425Firmware string + +var ( + errUnknownDevice = errors.New("bma42x: unknown device") + errUnsupportedDevice = errors.New("bma42x: device not part of config") + errConfigMismatch = errors.New("bma42x: config mismatch") + errTimeout = errors.New("bma42x: timeout") + errInitFailed = errors.New("bma42x: failed to initialize") +) + +const Address = 0x18 // BMA421/BMA425 address + +type DeviceType uint8 + +const ( + DeviceBMA421 DeviceType = 1 << iota + DeviceBMA425 + + AnyDevice = DeviceBMA421 | DeviceBMA425 + noDevice DeviceType = 0 +) + +// Features to enable while configuring the accelerometer. +type Features uint8 + +const ( + FeatureStepCounting = 1 << iota +) + +type Config struct { + // Which devices to support (OR the device types together as needed). + Device DeviceType + + // Which features to enable. With Features == 0, only the accelerometer will + // be enabled. + Features Features +} + +type Device struct { + bus drivers.I2C + address uint8 + accelData [6]byte + combinedTempSteps [5]uint8 // [0:3] steps, [4] temperature + dataBuf [2]byte +} + +func NewI2C(i2c drivers.I2C, address uint8) *Device { + return &Device{ + bus: i2c, + address: address, + } +} + +func (d *Device) Connected() bool { + val, err := d.read1(_CHIP_ID) + return err == nil && identifyChip(val) != noDevice +} + +func (d *Device) Configure(config Config) error { + if config.Device == 0 { + config.Device = AnyDevice + } + + // Check chip ID, to check the connection and to determine which BMA42x + // device we're dealing with. + chipID, err := d.read1(_CHIP_ID) + if err != nil { + return err + } + + // Determine which firmware (config file?) we'll be using. + // There is an extra check for the device before using the given firmware. + // This check will typically be optimized away if the given device is not + // configured, so that the firmware (which is 6kB in size!) won't be linked + // into the binary. + var firmware string + switch identifyChip(chipID) { + case DeviceBMA421: + if config.Device&DeviceBMA421 == 0 { + return errUnsupportedDevice + } + firmware = bma421Firmware + case DeviceBMA425: + if config.Device&DeviceBMA425 == 0 { + return errUnsupportedDevice + } + firmware = bma425Firmware + default: + return errUnknownDevice + } + + // Reset the chip, to be able to initialize it properly. + // The datasheet says a delay is needed after a SoftReset, but it doesn't + // say how long this delay should be. The bma423 driver however uses a 200ms + // delay, so that's what we'll be using. + err = d.write1(_CMD, cmdSoftReset) + if err != nil { + return err + } + time.Sleep(200 * time.Millisecond) + + // Disable power saving. + err = d.write1(_PWR_CONF, 0x00) + if err != nil { + return err + } + time.Sleep(450 * time.Microsecond) + + // Start initialization (because the datasheet says so). + err = d.write1(_INIT_CTRL, 0x00) + if err != nil { + return err + } + + // Write "config file" (actually a firmware, I think) to the chip. + // To do this, unsafely cast the string to a byte slice to avoid putting it + // in RAM. This is safe in this case because Tx won't write to the 'w' + // slice. + err = d.bus.Tx(uint16(d.address), unsafeStringToSlice(firmware), nil) + if err != nil { + return err + } + + // Read the config data back. + // We don't do that, as it slows down configuration and it probably isn't + // _really_ necessary with a reasonably stable I2C bus. + if false { + data := make([]byte, len(firmware)-1) + err = d.readn(_FEATURES_IN, data) + if err != nil { + return err + } + for i, c := range data { + if firmware[i+1] != c { + return errConfigMismatch + } + } + } + + // Enable sensors. + err = d.write1(_INIT_CTRL, 0x01) + if err != nil { + return err + } + + // Wait until the device is initialized. + start := time.Now() + status := uint8(0) // busy + for status == 0 { + status, err = d.read1(_INTERNAL_STATUS) + if err != nil { + return err // I2C bus error. + } + if status > 1 { + // Expected either 0 ("not_init") or 1 ("init_ok"). + return errInitFailed + } + if time.Since(start) >= 150*time.Millisecond { + // The datasheet says initialization should not take longer than + return errTimeout + } + // Don't bother the chip all the time while it's initializing. + time.Sleep(50 * time.Microsecond) + } + + if config.Features&FeatureStepCounting != 0 { + // Enable step counter. + // TODO: support step counter parameters. + var buf [71]byte + buf[0] = _FEATURES_IN // prefix buf with the command + data := buf[1:] + err = d.readn(_FEATURES_IN, data) + if err != nil { + return err + } + data[0x3A+1] |= 0x10 // enable step counting by setting a magical bit + err = d.bus.Tx(uint16(d.address), buf[:], nil) + if err != nil { + return err + } + } + + // Enable the accelerometer. + err = d.write1(_PWR_CTRL, 0x04) + if err != nil { + return err + } + + // Configure accelerometer for low power usage: + // acc_perf_mode=0 (power saving enabled) + // acc_bwp=osr4_avg1 (no averaging) + // acc_odr=50Hz (50Hz sampling interval, enough for the step counter) + const accelConf = 0x00<<7 | 0x00<<4 | 0x07<<0 + err = d.write1(_ACC_CONF, accelConf) + if err != nil { + return err + } + + // Reduce current consumption. + // With power saving enabled (and the above ACC_CONF) the chip consumes only + // 14µA. + err = d.write1(_PWR_CONF, 0x03) + if err != nil { + return err + } + + return nil +} + +func (d *Device) Update(which drivers.Measurement) error { + // TODO: combine temperature and step counter into a single read. + if which&drivers.Temperature != 0 { + val, err := d.read1(_TEMPERATURE) + if err != nil { + return err + } + d.combinedTempSteps[4] = val + } + if which&drivers.Acceleration != 0 { + // The acceleration data is stored in DATA8 through DATA13 as 3 12-bit + // values. + err := d.readn(_DATA_8, d.accelData[:]) // ACC_X(LSB) + if err != nil { + return err + } + err = d.readn(_STEP_COUNTER_0, d.combinedTempSteps[:4]) + if err != nil { + return err + } + } + return nil +} + +// Temperature returns the last read temperature in celsius milli degrees (1°C +// is 1000). +func (d *Device) Temperature() int32 { + // The temperature value is a two's complement number (meaning: signed) in + // units of 1 kelvin, with 0 being 23°C. + return (int32(int8(d.combinedTempSteps[4])) + 23) * 1000 +} + +// Acceleration returns the last read acceleration 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 *Device) Acceleration() (x, y, z int32) { + // Combine raw data from d.accelData (stored as 12-bit signed values) into a + // number (0..4095): + x = int32(d.accelData[0])>>4 | int32(d.accelData[1])<<4 + y = int32(d.accelData[2])>>4 | int32(d.accelData[3])<<4 + z = int32(d.accelData[4])>>4 | int32(d.accelData[5])<<4 + // Sign extend this number to -2048..2047: + x = (x << 20) >> 20 + y = (y << 20) >> 20 + z = (z << 20) >> 20 + // Scale from -512..511 to -1000_000..998_046. + // Or, at the maximum range (4g), from -2048..2047 to -2000_000..3998_046. + // The formula derived as follows (where 512 is the expected value at 1g): + // x = x * 1000_000 / 512 + // x = x * (1000_000/64) / (512/64) + // x = x * 15625 / 8 + x = x * 15625 / 8 + y = y * 15625 / 8 + z = z * 15625 / 8 + return +} + +// Steps returns the number of steps counted since the BMA42x sensor was +// initialized. +func (d *Device) Steps() (steps uint32) { + steps |= uint32(d.combinedTempSteps[0]) << 0 + steps |= uint32(d.combinedTempSteps[1]) << 8 + steps |= uint32(d.combinedTempSteps[2]) << 16 + steps |= uint32(d.combinedTempSteps[3]) << 24 + return +} + +func (d *Device) read1(register uint8) (uint8, error) { + d.dataBuf[0] = register + err := d.bus.Tx(uint16(d.address), d.dataBuf[:1], d.dataBuf[1:2]) + return d.dataBuf[1], err +} + +func (d *Device) readn(register uint8, data []byte) error { + d.dataBuf[0] = register + return d.bus.Tx(uint16(d.address), d.dataBuf[:1], data) +} + +func (d *Device) write1(register uint8, data uint8) error { + d.dataBuf[0] = register + d.dataBuf[1] = data + return d.bus.Tx(uint16(d.address), d.dataBuf[:2], nil) +} + +func unsafeStringToSlice(s string) []byte { + // TODO: use unsafe.Slice(unsafe.StringData(...)) once we require Go 1.20. + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + return unsafe.Slice((*byte)(unsafe.Pointer(sh.Data)), len(s)) +} + +func identifyChip(chipID uint8) DeviceType { + switch chipID { + case 0x11: + return DeviceBMA421 + case 0x13: + return DeviceBMA425 + default: + return noDevice + } +} diff --git a/bma42x/registers.go b/bma42x/registers.go new file mode 100644 index 000000000..aea75d58a --- /dev/null +++ b/bma42x/registers.go @@ -0,0 +1,73 @@ +package bma42x + +const ( + // I2C registers + _CHIP_ID = 0x00 + _ERR_REG = 0x02 + _STATUS = 0x03 + _DATA_0 = 0x0A + _DATA_1 = 0x0B + _DATA_2 = 0x0C + _DATA_3 = 0x0D + _DATA_4 = 0x0E + _DATA_5 = 0x0F + _DATA_6 = 0x10 + _DATA_7 = 0x11 + _DATA_8 = 0x12 + _DATA_9 = 0x13 + _DATA_10 = 0x14 + _DATA_11 = 0x15 + _DATA_12 = 0x16 + _DATA_13 = 0x17 + _SENSORTIME_0 = 0x18 + _SENSORTIME_1 = 0x19 + _SENSORTIME_2 = 0x1A + _EVENT = 0x1B + _INT_STATUS_0 = 0x1C + _INT_STATUS_1 = 0x1D + _STEP_COUNTER_0 = 0x1E + _STEP_COUNTER_1 = 0x1F + _STEP_COUNTER_2 = 0x20 + _STEP_COUNTER_3 = 0x21 + _TEMPERATURE = 0x22 + _FIFO_LENGTH_0 = 0x24 + _FIFO_LENGTH_1 = 0x25 + _FIFO_DATA = 0x26 + _ACTIVITY_TYPE = 0x27 + _INTERNAL_STATUS = 0x2A + _ACC_CONF = 0x40 + _ACC_RANGE = 0x41 + _AUX_CONF = 0x44 + _FIFO_DOWNS = 0x45 + _FIFO_WTM_0 = 0x46 + _FIFO_WTM_1 = 0x47 + _FIFO_CONFIG_0 = 0x48 + _FIFO_CONFIG_1 = 0x49 + _AUX_DEV_ID = 0x4B + _AUX_IF_CONF = 0x4C + _AUX_RD_ADDR = 0x4D + _AUX_WR_ADDR = 0x4E + _AUX_WR_DATA = 0x4F + _INT1_IO_CTRL = 0x53 + _INT2_IO_CTRL = 0x54 + _INT_LATCH = 0x55 + _INT1_MAP = 0x56 + _INT2_MAP = 0x57 + _INT_MAP_DATA = 0x58 + _INIT_CTRL = 0x59 + _FEATURES_IN = 0x5E + _INTERNAL_ERROR = 0x5F + _NVM_CONF = 0x6A + _IF_CONF = 0x6B + _ACC_SELF_TEST = 0x6D + _NV_CONF = 0x70 + _OFFSET_0 = 0x71 + _OFFSET_1 = 0x72 + _OFFSET_2 = 0x73 + _PWR_CONF = 0x7C + _PWR_CTRL = 0x7D + _CMD = 0x7E + + // Commands send to regCommand. + cmdSoftReset = 0xB6 +) diff --git a/examples/bma42x/main.go b/examples/bma42x/main.go new file mode 100644 index 000000000..8f0b4a0f3 --- /dev/null +++ b/examples/bma42x/main.go @@ -0,0 +1,54 @@ +package main + +// Smoke test for the BMA421/BMA425 sensors. +// Warning: this code has _not been tested_. It's only here as a smoke test. + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/bma42x" +) + +func main() { + time.Sleep(5 * time.Second) + + i2cBus := machine.I2C1 + i2cBus.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + SDA: machine.SDA_PIN, + SCL: machine.SCL_PIN, + }) + + sensor := bma42x.NewI2C(i2cBus, bma42x.Address) + err := sensor.Configure(bma42x.Config{ + Device: bma42x.DeviceBMA421 | bma42x.DeviceBMA425, + Features: bma42x.FeatureStepCounting, + }) + if err != nil { + println("could not configure BMA421/BMA425:", err) + return + } + + if !sensor.Connected() { + println("BMA42x not connected") + return + } + + for { + time.Sleep(time.Second) + + err := sensor.Update(drivers.Acceleration | drivers.Temperature) + if err != nil { + println("Error reading sensor", err) + continue + } + + fmt.Printf("Temperature: %.2f °C\n", float32(sensor.Temperature())/1000) + + accelX, accelY, accelZ := sensor.Acceleration() + fmt.Printf("Acceleration: %.2fg %.2fg %.2fg\n", float32(accelX)/1e6, float32(accelY)/1e6, float32(accelZ)/1e6) + } +} diff --git a/smoketest.sh b/smoketest.sh index 38bfed6e2..d2712e5ea 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -13,6 +13,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/apa tinygo build -size short -o ./build/test.hex -target=microbit ./examples/at24cx/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bh1750/main.go 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.hex -target=itsybitsy-m0 ./examples/bmp180/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go