Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bma42x: add new BMA421/BMA425 driver #587

Merged
merged 1 commit into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added bma42x/bma421-config-waspos.bin
Binary file not shown.
Binary file added bma42x/bma425-config-waspos.bin
Binary file not shown.
352 changes: 352 additions & 0 deletions bma42x/bma42x.go
Original file line number Diff line number Diff line change
@@ -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
}
}