-
Notifications
You must be signed in to change notification settings - Fork 197
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bma42x: add new BMA421/BMA425 driver
I wrote this for the PineTime, and all available sensors (accelerometer, step counter, temperature sensor) do work. This commit also includes two "configuration files", that actually appear to be firmware files to run on the accelerometer for special features like step counting. I'm not sure where they originally came for, and I don't know the copyright status of them. However, Bosch has open-sourced the BMA423 driver which includes a similar binary blob and the InfiniTime and Wasp-OS projects have been shipping these blobs without issues, so I think it's reasonably safe to include these binary blobs directly in the source.
- Loading branch information
Showing
6 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.