Files
pebble/src/fw/drivers/imu/bmi160/bmi160.c
T

1079 lines
36 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "bmi160.h"
#include "bmi160_private.h"
#include "drivers/accel.h"
#include "bmi160_regs.h"
#include "drivers/exti.h"
#include "drivers/rtc.h"
#include "system/passert.h"
#include "system/logging.h"
#include "kernel/util/delay.h"
#include "util/math.h"
#include "util/size.h"
#include "kernel/util/sleep.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
// Note: Before adding a new header, be sure you actually need it! The goal
// is to keep the driver as unreliant on higher level constructs as possible
#ifdef BMI160_DEBUG
#include "console/dbgserial.h"
#define BMI160_DBG(msg, ...) \
do { \
char _buf[80]; \
dbgserial_putstr_fmt(_buf, sizeof(_buf), msg, __VA_ARGS__); \
} while (0)
#else
#define BMI160_DBG(msg, ...)
#endif
#define NUM_AVERAGED_SAMPLES (4)
typedef enum {
BMI160_SCALE_2G = 2,
BMI160_SCALE_4G = 4,
BMI160_SCALE_8G = 8,
BMI160_SCALE_16G = 16,
} Bmi160Scale;
static int16_t s_raw_unit_to_mgs = 8192;
static BMI160AccelPowerMode s_accel_power_mode = BMI160_Accel_Mode_Suspend;
static BMI160GyroPowerMode s_gyro_power_mode = BMI160_Gyro_Mode_Suspend;
static bool s_accel_outstanding_motion_work = false;
static bool s_accel_outstanding_data_work = false;
static bool s_fifo_in_use = false;
static uint8_t curr_fifo_num_samples_wm = 0;
static bool s_double_tap_detection_enabled = false;
static bool s_shake_detection_enabled = false;
// Accelerometer configuration criteria
// Each operating mode can be enabled and disabled independently from each other and the driver
// will configure the accelerometer in the highest power mode and with the highest sampling rate
// required according which operating modes are enabled and what the requirements are thereof.
typedef enum {
AccelOperatingModeData,
AccelOperatingModeShakeDetection,
AccelOperatingModeDoubleTapDetection,
AccelOperatingMode_Num,
} AccelOperatingMode;
typedef enum {
AccelPowerModeLowPower,
AccelPowerModeNormal,
AccelPowerMode_Num,
} AccelPowerMode;
static struct {
bool enabled;
BMI160SampleRate sample_interval;
} s_operating_states[] = {
[AccelOperatingModeData] = {
.enabled = false,
.sample_interval = BMI160SampleRate_25_HZ,
},
[AccelOperatingModeShakeDetection] = {
.enabled = false,
.sample_interval = BMI160SampleRate_25_HZ,
},
[AccelOperatingModeDoubleTapDetection] = {
.enabled = false,
.sample_interval = BMI160SampleRate_200_HZ,
},
};
#define HZ_TO_US(hz) 1000000 / (hz)
static void prv_write_reg(uint8_t reg, uint8_t value) {
bmi160_write_reg(reg, value);
// Wait 2 us (active mode) or 450 us (suspend mode)
// before issuing the next read or write command.
//
// TODO: I'm pretty sure if commands are specifically targetting
// a unit in suspend mode, we will need to delay for 450us even if
// the other unit is powered up in Normal mode
if (s_accel_power_mode == BMI160_Accel_Mode_Normal
|| s_gyro_power_mode == BMI160_Gyro_Mode_Normal) {
// Apparently this delays for ~3.5 us. Unconfirmed.
delay_us(5);
} else {
psleep(2); // must sleep >= 450us
}
}
static void prv_burst_read(uint8_t reg, void *buf, size_t len) {
reg |= BMI160_READ_FLAG;
SPIScatterGather sg_info[2] = {
{.sg_len = 1, .sg_out = &reg, .sg_in = NULL}, // address
{.sg_len = len, .sg_out = NULL, .sg_in = buf} // read data
};
spi_slave_burst_read_write_scatter(BMI160_SPI, sg_info, ARRAY_LENGTH(sg_info));
}
static void prv_read_modify_write(uint8_t reg, uint8_t value, uint8_t mask) {
uint8_t new_val = bmi160_read_reg(reg);
new_val &= ~mask;
new_val |= value;
prv_write_reg(reg, new_val);
}
static void prv_run_command(uint8_t command) {
prv_write_reg(BMI160_REG_CMD, command);
if (command == BMI160_CMD_SOFTRESET) {
s_accel_power_mode = BMI160_Accel_Mode_Suspend;
s_gyro_power_mode = BMI160_Gyro_Mode_Suspend;
}
}
static Bmi160Scale prv_get_accel_scale(void) {
uint8_t scale_reg_val = bmi160_read_reg(BMI160_REG_ACC_RANGE) & 0xf;
if (scale_reg_val == BMI160_ACC_RANGE_2G) {
return BMI160_SCALE_2G;
} else if (scale_reg_val == BMI160_ACC_RANGE_4G) {
return BMI160_SCALE_4G;
} else if (scale_reg_val == BMI160_ACC_RANGE_8G) {
return BMI160_SCALE_8G;
} else if (scale_reg_val == BMI160_ACC_RANGE_16G) {
return BMI160_SCALE_16G;
}
WTF;
return 0;
}
static void prv_set_accel_scale(Bmi160Scale scale) {
uint8_t cfg_val;
switch (scale) {
case BMI160_SCALE_2G:
cfg_val = BMI160_ACC_RANGE_2G;
break;
case BMI160_SCALE_4G:
cfg_val = BMI160_ACC_RANGE_4G;
break;
case BMI160_SCALE_8G:
cfg_val = BMI160_ACC_RANGE_8G;
break;
case BMI160_SCALE_16G:
cfg_val = BMI160_ACC_RANGE_16G;
break;
default:
WTF;
}
prv_write_reg(BMI160_REG_ACC_RANGE, cfg_val);
s_raw_unit_to_mgs = 32768 / scale;
}
static int16_t prv_raw_to_mgs(int16_t raw_val) {
int16_t mgs = (raw_val * 1000) / s_raw_unit_to_mgs;
return mgs;
}
static void prv_convert_accel_raw_data_to_mgs(const uint8_t *raw_buf,
AccelDriverSample *data) {
int16_t readings[3];
for (unsigned int i = 0; i < ARRAY_LENGTH(readings); i++) {
int base = i * 2;
readings[i] = raw_buf[base] | (raw_buf[base + 1] << 8);
}
const AccelConfig *cfg = &BOARD_CONFIG_ACCEL.accel_config;
data->x = (cfg->axes_inverts[AXIS_X] ? -1 : 1) *
prv_raw_to_mgs(readings[cfg->axes_offsets[AXIS_X]]);
data->y = (cfg->axes_inverts[AXIS_Y] ? -1 : 1) *
prv_raw_to_mgs(readings[cfg->axes_offsets[AXIS_Y]]);
data->z = (cfg->axes_inverts[AXIS_Z] ? -1 : 1) *
prv_raw_to_mgs(readings[cfg->axes_offsets[AXIS_Z]]);
}
static uint64_t prv_get_curr_system_time_ms(void) {
time_t time_s;
uint16_t time_ms;
rtc_get_time_ms(&time_s, &time_ms);
return (((uint64_t)time_s) * 1000 + time_ms);
}
static uint32_t prv_sensortime_to_timestamp(uint8_t sensor_time[3]) {
return (sensor_time[0] | (sensor_time[1] << 8) | (sensor_time[2] << 16));
}
static uint32_t prv_get_min_sampling_interval_us(void) {
BMI160SampleRate sample_rate_max = BMI160SampleRate_12p5_HZ;
for (uint32_t i = 0; i < ARRAY_LENGTH(s_operating_states); i++) {
if (s_operating_states[i].enabled) {
// use MIN because sample rate enum value is actually the sampling interval in us
sample_rate_max = MIN(sample_rate_max, s_operating_states[i].sample_interval);
}
}
return (uint32_t)sample_rate_max;
}
// Determine the bit that flips when the sample is collected (as sensor events
// are synchronous to this register)
static uint8_t prv_get_sample_collection_bit(void) {
return (31 - (__builtin_clz(prv_get_min_sampling_interval_us() /
BMI160_SENSORTIME_RESOLUTION_US)));
}
// Converts BMI160 sensortime data to a uint32_t (units incremented every 39us)
static uint32_t prv_get_time_since_sample(uint8_t sensor_time[3]) {
uint32_t sensor_timestamp = prv_sensortime_to_timestamp(sensor_time);
uint8_t sample_time_bit = prv_get_sample_collection_bit();
uint64_t time_since_data_collection_us = (sensor_timestamp &
((0x1 << sample_time_bit) - 1)) * BMI160_SENSORTIME_RESOLUTION_US;
return time_since_data_collection_us;
}
// determines if a new sample was collected between the two sensor timestamps
// provided
static bool prv_new_sample_collected(uint8_t sensor_timestamp_before[3],
uint8_t sensor_timestamp_after[3]) {
uint32_t start_time = prv_sensortime_to_timestamp(sensor_timestamp_before);
uint32_t end_time = prv_sensortime_to_timestamp(sensor_timestamp_after);
uint32_t sample_time_bit = prv_get_sample_collection_bit();
// see if the upper bits oveflowed
uint32_t upper_bits_mask = ~((0x1 << sample_time_bit) - 1);
start_time &= upper_bits_mask;
end_time &= upper_bits_mask;
return (start_time != end_time);
}
// Converts the sensor time from the BMI160 captured at the time the sample
// was collected to the actual system time
static uint64_t prv_get_sample_time_us(uint8_t sensor_time[3]) {
uint64_t curr_time_us = prv_get_curr_system_time_ms() * 1000;
uint32_t time_since_data_collection_us = prv_get_time_since_sample(sensor_time);
BMI160_DBG("%"PRIu32" us delay since sample was collected",
time_since_data_collection_us);
return (curr_time_us - time_since_data_collection_us);
}
static void prv_read_curr_accel_data(AccelDriverSample *data) {
uint8_t res[9]; /* x, y, z & timestamp */
prv_burst_read(BMI160_REG_ACC_X_LSB, res, sizeof(res));
*data = (AccelDriverSample){};
prv_convert_accel_raw_data_to_mgs(res, data);
data->timestamp_us = prv_get_sample_time_us(&res[6]);
BMI160_DBG("%"PRId16" %"PRId16" %"PRId16, data->x, data->y, data->z);
}
static IMUCoordinateAxis prv_get_axis_direction(uint8_t int0_status, uint8_t int2_status,
uint8_t mask, uint8_t shift, int32_t *direction) {
*direction = ((int2_status & BMI160_INT_STATUS_2_ANYM_SIGN) == 0) ? 1 : -1;
IMUCoordinateAxis axis = AXIS_X;
const AccelConfig *cfg = &BOARD_CONFIG_ACCEL.accel_config;
bool invert = false;
if ((int2_status & (shift << cfg->axes_offsets[AXIS_X])) != 0) {
axis = AXIS_X;
invert = cfg->axes_inverts[AXIS_X];
} else if ((int2_status & (shift << cfg->axes_offsets[AXIS_Y])) != 0) {
axis = AXIS_Y;
invert = cfg->axes_inverts[AXIS_Y];
} else if ((int2_status & (shift << cfg->axes_offsets[AXIS_Z])) != 0) {
axis = AXIS_Z;
invert = cfg->axes_inverts[AXIS_Z];
} else {
BMI160_DBG("No Axis?: 0x%"PRIx8" 0x%"PRIx8, int0_status, int2_status);
}
*direction *= (invert ? -1 : 1);
return axis;
}
static void prv_dump_int_stats(void);
static void prv_handle_motion_interrupts(void) {
s_accel_outstanding_motion_work = false;
// Interestingly, the status registers for tap interrupts are updated _after_
// the EXTI fires. Low power mode toggles between suspend and normal mode. I
// believe the updates to the registers only occur during the run cycles
// which occur at an interval dependent on the sampling frequency
bool toggled_power_mode = (s_accel_power_mode == BMI160_Accel_Mode_Low);
if (toggled_power_mode) {
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Normal);
}
uint8_t int0_status = bmi160_read_reg(BMI160_REG_INT_STATUS_0);
uint8_t int2_status = bmi160_read_reg(BMI160_REG_INT_STATUS_2);
prv_run_command(BMI160_CMD_INT_RESET);
// debug
bool anymotion = ((int0_status & BMI160_INT_STATUS_0_ANYM_MASK) != 0);
if (anymotion) {
int32_t direction;
IMUCoordinateAxis axis = prv_get_axis_direction(int0_status, int2_status,
BMI160_INT_STATUS_2_ANYM_SIGN,
BMI160_INT_STATUS_2_ANYM_FIRST_X, &direction);
BMI160_DBG("Anymotion on axis %"PRIu8" in direction %"PRId32, axis, direction);
accel_cb_shake_detected(axis, direction);
}
bool double_tap = ((int0_status & BMI160_INT_STATUS_0_D_TAP_MASK) != 0);
if (double_tap) {
int32_t direction;
IMUCoordinateAxis axis = prv_get_axis_direction(int0_status, int2_status,
BMI160_INT_STATUS_2_TAP_SIGN,
BMI160_INT_STATUS_2_TAP_FIRST_X, &direction);
if (double_tap) {
BMI160_DBG("Double tap on axis %"PRIu8" in direction %"PRId32, axis, direction);
accel_cb_double_tap_detected(axis, direction);
}
} else if (!anymotion) {
BMI160_DBG("Wahh, no motion/tap?: INT0: 0x%"PRIx8", INT2: 0x%"PRIx8, int0_status, int2_status);
prv_dump_int_stats();
}
if (toggled_power_mode) {
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Low);
}
}
// TODO: strictly for debug, remove when done
static void prv_dump_int_stats(void) {
for (uint8_t addr = 0x1b; addr <= 0x1f; addr++) {
BMI160_DBG("0x%"PRIx8" = 0x%"PRIx8, addr, bmi160_read_reg(addr));
}
BMI160_DBG("Latched = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_LATCH));
BMI160_DBG("Err reg = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_ERR));
BMI160_DBG("INT_MAP[0] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_MAP_0));
BMI160_DBG("INT_MAP[1] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_MAP_1));
BMI160_DBG("INT_EN[0] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_EN_0));
BMI160_DBG("INT_EN[1] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_EN_1));
}
// should be servicing tap/motion interrupts
static void bmi160_IRQ1_handler(bool *should_context_switch) {
if (!s_accel_outstanding_motion_work) {
s_accel_outstanding_motion_work = true;
accel_offload_work_from_isr(prv_handle_motion_interrupts, should_context_switch);
} else {
BMI160_DBG("%s", "We fell behind on motion interrupt handling!");
}
}
static uint8_t prv_get_fifo_frame_size(void) {
return 6; // we are just storing {x, y, z} accel data in the fifo today
}
static uint16_t prv_get_current_fifo_length_and_timestamp(uint64_t *timestamp_us) {
int retries = 0;
while (retries < 10) {
uint8_t ts_before[3], ts_after[3];
// We want to find the timestamp of the latest sample in the fifo so:
// 1. read the current sensor timestamp
// 2. read the current length of the fifo
// 3. read the sensor timestamp again
// Since new sample collection is synchronous with a particular bit of the
// sensor timestamp, we can see if that bit overflowed between 1 & 3 to see
// if a new sample was appended. Continue this process until there is no
// overflow between the readings in 1 & 3.
prv_burst_read(BMI160_REG_SENSORTIME_0, &ts_before[0], sizeof(ts_before));
uint64_t sample_time_before = prv_get_sample_time_us(ts_before);
uint16_t num_samples = bmi160_read_16bit_reg(BMI160_REG_FIFO_LENGTH_LSB);
prv_burst_read(BMI160_REG_SENSORTIME_0, &ts_after[0], sizeof(ts_after));
// check to see if we rolled
if (!prv_new_sample_collected(ts_before, ts_after)) {
*timestamp_us = sample_time_before;
return (num_samples);
}
retries++;
};
// something has gone wrong if we fail to recover the right length & timestamp
PBL_ASSERTN(retries < 10);
return 0;
}
static void prv_process_fifo_frame(const uint8_t *frame_buf, AccelDriverSample *data) {
const int a_begin = 0; // index within the frame where accel data starts
prv_convert_accel_raw_data_to_mgs(&frame_buf[a_begin], data);
}
static void prv_drain_fifo(void) {
// we can't drain the fifo if we are in low power mode so we have
// to temporarily enter normal mode
bool was_low_power = (s_accel_power_mode == BMI160_Accel_Mode_Low);
if (was_low_power) {
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Normal);
}
// get the FIFO length
uint64_t last_frame_time = 0;
uint16_t len = prv_get_current_fifo_length_and_timestamp(&last_frame_time);
BMI160_DBG("Reading %d bytes", len);
uint8_t fifo_frame_len = prv_get_fifo_frame_size();
bmi160_begin_burst(BMI160_REG_FIFO_DATA | BMI160_READ_FLAG);
uint8_t curr_num_samples = (len / fifo_frame_len);
uint32_t curr_sampling_interval_us = prv_get_min_sampling_interval_us();
uint64_t start_time = last_frame_time - curr_num_samples * curr_sampling_interval_us;
for (int i = 0; i < len; i += fifo_frame_len) {
uint8_t burst_buf[fifo_frame_len];
spi_ll_slave_burst_read(BMI160_SPI, &burst_buf[0], fifo_frame_len);
AccelDriverSample data;
prv_process_fifo_frame(burst_buf, &data);
data.timestamp_us = start_time;
start_time += curr_sampling_interval_us;
BMI160_DBG("%2d: %"PRId16" %"PRId16" %"PRId16, i, data.x, data.y, data.z);
accel_cb_new_sample(&data);
}
bmi160_end_burst();
BMI160_DBG("%d bytes remain", prv_get_current_fifo_length_and_timestamp(&last_frame_time));
if (was_low_power) {
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Low);
}
}
static void prv_handle_data(void) {
s_accel_outstanding_data_work = false;
// if the task draining the fifo gets swapped out for a long enough duration,
// its possible the fifo watermark interrupt may fire multiple times. Once
// the task finishes draining the fifo, the interrupt will be cleared but a
// fifo drain callback could have already been scheduled so don't check the
// interrupt status
if (s_fifo_in_use) {
prv_drain_fifo();
return;
}
// the int_status for drdy is not latched so we check the status register
// instead to confirm new data accel data is available
uint8_t status = bmi160_read_reg(BMI160_REG_STATUS);
if ((status & BMI160_STATUS_DRDY_ACC_MASK) != 0) {
AccelDriverSample data;
prv_read_curr_accel_data(&data);
accel_cb_new_sample(&data);
} else {
BMI160_DBG("Unexpected int status: 0x%"PRIx8" 0x%x",
bmi160_read_reg(BMI160_REG_INT_STATUS_1), status);
}
}
static void bmi160_IRQ2_handler(bool *should_context_switch) {
if (!s_accel_outstanding_data_work) {
s_accel_outstanding_data_work = true;
accel_offload_work_from_isr(prv_handle_data, should_context_switch);
} else {
BMI160_DBG("%s", "We fell behind on data handling");
}
}
// in order to actually enter 'low power' mode, we have to set up accel to do
// undersampling. The more samples we use for one reading, the higher the power
// consumption but the lower the RMS noise
static void prv_accel_enable_undersampling(bool enable) {
const uint8_t acc_us_bwp_mask =
(BMI160_ACC_CONF_ACC_BWP_MASK << BMI160_ACC_CONF_ACC_BWP_SHIFT) |
(BMI160_ACC_CONF_ACC_US_MASK << BMI160_ACC_CONF_ACC_US_SHIFT);
uint8_t acc_us_bwp;
if (!enable) {
acc_us_bwp = 0x2 << BMI160_ACC_CONF_ACC_BWP_SHIFT;
} else {
_Static_assert(NUM_AVERAGED_SAMPLES <= 128, "Number of averaged samples "
"must be <= 128");
const uint8_t acc_bwp = 31 - __builtin_clz((uint32_t)NUM_AVERAGED_SAMPLES);
acc_us_bwp = (0x1 << BMI160_ACC_CONF_ACC_US_SHIFT) |
(acc_bwp << BMI160_ACC_CONF_ACC_BWP_SHIFT);
}
prv_read_modify_write(BMI160_REG_ACC_CONF, acc_us_bwp, acc_us_bwp_mask);
}
static void prv_update_accel_interrupts(bool enable) {
for (unsigned int i = 0; i < ARRAY_LENGTH(BOARD_CONFIG_ACCEL.accel_ints); i++) {
uint8_t int_cfg = 0;
uint8_t int_mask = 0xf << (i * 4);
if (enable) {
int_cfg = 0xA << (i * 4); // INT EN, Push-Pull, Active High, Level Triggered
exti_enable(BOARD_CONFIG_ACCEL.accel_ints[i]);
} else { // disable
exti_disable(BOARD_CONFIG_ACCEL.accel_ints[i]);
}
prv_read_modify_write(BMI160_REG_INT_OUT_CTRL, int_cfg, int_mask);
BMI160_DBG("INT_OUT_CTRL = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_OUT_CTRL));
}
}
void bmi160_init(void) {
gpio_input_init(&BOARD_CONFIG_ACCEL.accel_int_gpios[0]);
gpio_input_init(&BOARD_CONFIG_ACCEL.accel_int_gpios[1]);
exti_configure_pin(BOARD_CONFIG_ACCEL.accel_ints[0], ExtiTrigger_Rising,
bmi160_IRQ1_handler);
exti_configure_pin(BOARD_CONFIG_ACCEL.accel_ints[1], ExtiTrigger_Rising,
bmi160_IRQ2_handler);
bmi160_enable_spi_mode();
if (bmi160_query_whoami()) {
prv_run_command(BMI160_CMD_SOFTRESET);
bmi160_enable_spi_mode();
} else {
PBL_LOG(LOG_LEVEL_WARNING, "Failed to query BMI160");
}
prv_set_accel_scale(BMI160_SCALE_4G);
}
bool bmi160_query_whoami(void) {
uint8_t whoami = bmi160_read_reg(BMI160_REG_CHIP_ID);
PBL_LOG(LOG_LEVEL_DEBUG, "Read BMI160 whoami byte 0x%"PRIx8", expecting 0x%"PRIx8,
whoami, BMI160_CHIP_ID);
return (whoami == BMI160_CHIP_ID);
}
// TODO/NOTE: The accel & gyro self test routines changes some of the BMI160
// configuration state. In the future we could update them so they do not
// destroy the state
bool accel_run_selftest(void) {
prv_update_accel_interrupts(false);
prv_run_command(BMI160_CMD_SOFTRESET);
psleep(50);
bmi160_enable_spi_mode();
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Normal);
psleep(10);
// Set to 8g range, as required for the self test mode
prv_set_accel_scale(BMI160_SCALE_8G);
// Set ODR to 1600Hz
accel_set_sampling_interval(BMI160SampleRate_1600_HZ);
prv_accel_enable_undersampling(false);
PBL_LOG(LOG_LEVEL_DEBUG, "Self Test: Negative offset");
// Enable self test with high amplitude in the negative direction
prv_write_reg(BMI160_REG_SELF_TEST, 0x8 | 0b01);
psleep(50);
struct {
char axis_name;
uint8_t register_address;
int pass_threshold;
int16_t negative_value;
int16_t positive_value;
} accel_test_axis[] = {
{
.axis_name = 'X',
.register_address = BMI160_REG_ACC_X_LSB,
.pass_threshold = 3277
}, {
.axis_name = 'Y',
.register_address = BMI160_REG_ACC_Y_LSB,
.pass_threshold = 3277
}, {
.axis_name = 'Z',
.register_address = BMI160_REG_ACC_Z_LSB,
.pass_threshold = 1639
}
};
// Collect data with the negative offset applied
for (unsigned int i = 0; i < ARRAY_LENGTH(accel_test_axis); ++i) {
accel_test_axis[i].negative_value = bmi160_read_16bit_reg(accel_test_axis[i].register_address);
PBL_LOG(LOG_LEVEL_DEBUG, "- %c: %"PRId16,
accel_test_axis[i].axis_name, accel_test_axis[i].negative_value);
}
PBL_LOG(LOG_LEVEL_DEBUG, "Self Test: Positive offset");
// Flip sign bit from negative to positive while leaving self test mode on at high amplitude
prv_write_reg(BMI160_REG_SELF_TEST, 0x8 | 0x4 | 0b01);
psleep(50);
// Collect data with the positive offset applied
for (unsigned int i = 0; i < ARRAY_LENGTH(accel_test_axis); ++i) {
accel_test_axis[i].positive_value = bmi160_read_16bit_reg(accel_test_axis[i].register_address);
PBL_LOG(LOG_LEVEL_DEBUG, "+ %c: %"PRId16,
accel_test_axis[i].axis_name, accel_test_axis[i].positive_value);
}
// Verify each axis saw a big enough delta in response to the self test mode.
// NOTE! For some reason, applying a "positive" force makes the number go lower and applying
// a "negative" force makes the number go higher. And then, for some reason, we abs() it when
// calculating a delta to hide the fact that it's backwards. This is all documented in a
// document called "How to perform BMI160 accelerometer self-test" provided by Bosch, so I guess
// it's the right thing to do. This document is attached to PBL-10951.
bool pass = true;
for (unsigned int i = 0; i < ARRAY_LENGTH(accel_test_axis); ++i) {
int axis_delta = accel_test_axis[i].positive_value - accel_test_axis[i].negative_value;
axis_delta = abs(axis_delta);
if (axis_delta < accel_test_axis[i].pass_threshold) {
PBL_LOG(LOG_LEVEL_WARNING, "Self test failed for axis %c: %d < %d",
accel_test_axis[i].axis_name, axis_delta, accel_test_axis[i].pass_threshold);
pass = false;
}
}
prv_run_command(BMI160_CMD_SOFTRESET);
psleep(50);
bmi160_enable_spi_mode();
return pass;
}
bool gyro_run_selftest(void) {
prv_update_accel_interrupts(false);
prv_run_command(BMI160_CMD_SOFTRESET);
psleep(50);
bmi160_enable_spi_mode();
bmi160_set_gyro_power_mode(BMI160_Gyro_Mode_Normal);
// Write the gyr_self_test_start bit
prv_write_reg(BMI160_REG_SELF_TEST, 0x10);
psleep(50);
const uint8_t status = bmi160_read_reg(BMI160_REG_SELF_TEST);
// power down the gyro
bmi160_set_gyro_power_mode(BMI160_Gyro_Mode_Suspend);
if (status | 0x2) {
return true;
}
return false;
}
void bmi160_set_accel_power_mode(BMI160AccelPowerMode mode) {
uint8_t status = 0;
int retries = 10;
prv_run_command(BMI160_CMD_ACC_SET_PMU_MODE | mode);
while (retries--) {
// Takes 3.2 to 3.8ms according to datasheet
status = bmi160_read_reg(BMI160_REG_PMU_STATUS) >> 4;
if (status == mode) {
break;
}
BMI160_DBG("ACCEL: want mode %d, actual %d", mode, status);
}
#ifdef BMI160_DEBUG
PBL_ASSERT(retries > 0, "Could not set power mode to %d", mode);
#endif
s_accel_power_mode = mode;
BMI160_DBG("PMU_STATUS: 0x%x ACC_CONF: 0x%x",
bmi160_read_reg(BMI160_REG_PMU_STATUS), bmi160_read_reg(BMI160_REG_ACC_CONF));
}
void bmi160_set_gyro_power_mode(BMI160GyroPowerMode mode) {
int retries = 20;
prv_run_command(BMI160_CMD_GYR_SET_PMU_MODE | mode);
while (retries--) {
uint8_t status = 0;
// can take up to 80ms to power up
status = bmi160_read_reg(BMI160_REG_PMU_STATUS) >> 2;
if (status == mode) {
break;
}
psleep(5);
BMI160_DBG("GYRO: want mode %d, actual %d", mode, status);
}
PBL_ASSERT(retries > 0, "Gyro: Could not set power mode to %d", mode);
s_gyro_power_mode = mode;
BMI160_DBG("PMU_STATUS: 0x%x", bmi160_read_reg(BMI160_REG_PMU_STATUS));
}
/*
* accel.h driver interface exposed to higher level code
*/
static uint32_t prv_get_sampling_interval_from_hw(void) {
uint8_t acc_cfg = bmi160_read_reg(BMI160_REG_ACC_CONF);
acc_cfg = (acc_cfg >> BMI160_ACC_CONF_ACC_ODR_SHIFT) &
BMI160_ACC_CONF_ACC_ODR_MASK;
// sample interval (us) = 10000 * (2 ^ (8 - val(acc_odr)))
int shift_val = 8 - acc_cfg;
uint32_t interval = 10000;
if (shift_val >= 0) {
interval <<= shift_val;
} else {
interval >>= -shift_val;
}
return interval;
}
static BMI160AccODR prv_get_odr(BMI160SampleRate sample_rate) {
// sample rate = 100 / 2^(8- val(acc_odr))
BMI160AccODR acc_odr = 0;
if (BMI160SampleRate_12p5_HZ == sample_rate) {
acc_odr = BMI160AccODR_12p5_HZ;
} else if (BMI160SampleRate_25_HZ == sample_rate) {
acc_odr = BMI160AccODR_25_HZ;
} else if (BMI160SampleRate_50_HZ == sample_rate) {
acc_odr = BMI160AccODR_50_HZ;
} else if (BMI160SampleRate_100_HZ == sample_rate) {
acc_odr = BMI160AccODR_100_HZ;
} else if (BMI160SampleRate_200_HZ == sample_rate) {
acc_odr = BMI160AccODR_200_HZ;
} else if (BMI160SampleRate_400_HZ == sample_rate) {
acc_odr = BMI160AccODR_400_HZ;
} else if (BMI160SampleRate_800_HZ == sample_rate) {
acc_odr = BMI160AccODR_800_HZ;
} else { // any interval < min supported must saturate to 1600Hz
acc_odr = BMI160AccODR_1600_HZ;
}
return acc_odr;
}
static BMI160SampleRate prv_get_supported_sample_rate(uint32_t interval_us) {
BMI160SampleRate sample_rate;
if (BMI160SampleRate_12p5_HZ <= interval_us) {
sample_rate = BMI160SampleRate_12p5_HZ;
} else if (BMI160SampleRate_25_HZ <= interval_us) {
sample_rate = BMI160SampleRate_25_HZ;
} else if (BMI160SampleRate_50_HZ <= interval_us) {
sample_rate = BMI160SampleRate_50_HZ;
} else if (BMI160SampleRate_100_HZ <= interval_us) {
sample_rate = BMI160SampleRate_100_HZ;
} else if (BMI160SampleRate_200_HZ <= interval_us) {
sample_rate = BMI160SampleRate_200_HZ;
} else if (BMI160SampleRate_400_HZ <= interval_us) {
sample_rate = BMI160SampleRate_400_HZ;
} else if (BMI160SampleRate_800_HZ <= interval_us) {
sample_rate = BMI160SampleRate_800_HZ;
} else { // any interval < min supported must saturate to 1600Hz
sample_rate = BMI160SampleRate_1600_HZ;
}
return sample_rate;
}
static void prv_configure_operating_mode(void) {
BMI160SampleRate interval_us = prv_get_min_sampling_interval_us();
uint8_t acc_odr = (uint8_t)prv_get_odr(interval_us);
// should be able to write the sample range at any time
prv_read_modify_write(BMI160_REG_ACC_CONF, acc_odr,
BMI160_ACC_CONF_ACC_ODR_MASK << BMI160_ACC_CONF_ACC_ODR_SHIFT);
#ifdef BMI160_DEBUG
PBL_ASSERTN(interval_us == prv_get_sampling_interval_from_hw());
#endif
BMI160_DBG("Set sampling rate to %"PRIu32, (uint32_t)(1000000/interval_us));
if (s_accel_power_mode == BMI160_Accel_Mode_Normal) {
// This should only execute on startup or if the power mode is left in normal power mode for
// some reason
prv_accel_enable_undersampling(true);
bmi160_set_accel_power_mode(BMI160_Accel_Mode_Low);
BMI160_DBG("%s", "Enable low power mode");
}
// TODO: If we aren't doing anything else, suspend the chip?
}
static void prv_enable_operating_mode(AccelOperatingMode mode, BMI160SampleRate sample_rate) {
s_operating_states[mode].enabled = true;
s_operating_states[mode].sample_interval = sample_rate;
prv_configure_operating_mode();
}
static void prv_disable_operating_mode(AccelOperatingMode mode) {
s_operating_states[mode].enabled = false;
prv_configure_operating_mode();
}
uint32_t accel_set_sampling_interval(uint32_t interval_us) {
BMI160SampleRate sample_rate = prv_get_supported_sample_rate(interval_us);
prv_enable_operating_mode(AccelOperatingModeData, sample_rate);
return prv_get_min_sampling_interval_us();
}
uint32_t accel_get_sampling_interval(void) {
uint32_t curr_sampling_interval_us = prv_get_min_sampling_interval_us();
#ifdef BMI160_DEBUG
PBL_ASSERTN(curr_sampling_interval_us == prv_get_sampling_interval_from_hw());
#endif
return curr_sampling_interval_us;
}
static void prv_configure_accel_sampling(bool enable_int, bool use_fifo) {
static bool int_enabled = false;
if ((int_enabled == enable_int) && (s_fifo_in_use == use_fifo)) {
return; // No changes to make so don't redo writes
}
uint8_t map_mask =
BMI160_INT_MAP_1_INT2_DATA_READY | BMI160_INT_MAP_1_INT2_FIFO_WATERMARK;
uint8_t int_en_mask =
BMI160_INT_EN_1_DRDY_EN | BMI160_INT_EN_1_FWM_EN;
uint8_t map_val;
uint8_t int_en_val;
if (!enable_int) {
map_val = int_en_val = 0;
} else if (!use_fifo) {
map_val = BMI160_INT_MAP_1_INT2_DATA_READY;
int_en_val = BMI160_INT_EN_1_DRDY_EN;
} else {
map_val = BMI160_INT_MAP_1_INT2_FIFO_WATERMARK;
int_en_val = BMI160_INT_EN_1_FWM_EN;
}
prv_read_modify_write(BMI160_REG_INT_MAP_1, map_val, map_mask);
prv_read_modify_write(BMI160_REG_INT_EN_1, int_en_val, int_en_mask);
BMI160_DBG("INT_MAP[1] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_MAP_1));
BMI160_DBG("INT_EN[1] = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_INT_EN_1));
int_enabled = enable_int;
s_fifo_in_use = use_fifo;
}
void accel_set_num_samples(uint32_t num_samples) {
uint8_t fifo_frame_size = prv_get_fifo_frame_size(); // x,y,z accel data
uint16_t max_num_samples = (BMI160_FIFO_LEN_BYTES / fifo_frame_size);
max_num_samples = (max_num_samples / BMI160_FIFO_WM_UNIT_BYTES) *
BMI160_FIFO_WM_UNIT_BYTES;
if (num_samples > max_num_samples) {
num_samples = max_num_samples;
}
uint16_t curr_sample_size = bmi160_read_reg(BMI160_REG_FIFO_CONFIG_0) *
BMI160_FIFO_WM_UNIT_BYTES;
if (curr_sample_size > num_samples) { // flush what we have in the fifo, if any
BMI160_DBG("Curr Sample Size = %"PRId16, curr_sample_size);
prv_drain_fifo();
}
if (num_samples < 2) {
prv_write_reg(BMI160_REG_FIFO_CONFIG_1, 0); // power down the fifo
curr_fifo_num_samples_wm = 0;
} else {
curr_fifo_num_samples_wm = num_samples;
// Set the new fifo watermark
// TODO: I think we will want to try and make this a multiple of the frame size
uint8_t fifo_wm = (num_samples * fifo_frame_size) / BMI160_FIFO_WM_UNIT_BYTES;
prv_write_reg(BMI160_REG_FIFO_CONFIG_0, fifo_wm);
BMI160_DBG("FWM = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_FIFO_CONFIG_0));
uint8_t curr_frame_cfg = bmi160_read_reg(BMI160_REG_FIFO_CONFIG_1);
uint8_t desired_cfg = BMI160_FIFO_CONFIG_1_ACC_EN;
if (curr_frame_cfg != desired_cfg) {
prv_run_command(BMI160_CMD_FIFO_FLUSH); // clear any lingering entries
prv_write_reg(BMI160_REG_FIFO_CONFIG_1, desired_cfg);
}
}
bool enable_int = num_samples != 0;
bool use_fifo = num_samples > 1;
prv_configure_accel_sampling(enable_int, use_fifo);
}
int accel_peek(AccelDriverSample *data) {
prv_read_curr_accel_data(data);
return 0;
}
void accel_set_shake_sensitivity_high(bool sensitivity_high) {
// Configure the threshold level at which the BMI160 will think shake has occurred
if (sensitivity_high) {
prv_write_reg(BMI160_REG_INT_MOTION_1,
BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdLow]);
} else {
prv_write_reg(BMI160_REG_INT_MOTION_1,
BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdHigh]);
}
}
static void prv_enable_shake_detection(void) {
// don't automatically power-up the gryo when an anymotion interrupt fires!
prv_write_reg(BMI160_REG_PMU_TRIGGER, 0x0);
// Enable the anymotion detection interrupt
uint8_t en_val = BMI160_INT_MAP_ANYMOTION_EN_MASK;
prv_read_modify_write(BMI160_REG_INT_MAP_0, en_val, en_val);
// Actually enable the anymotion interrupt
uint8_t int_en = (BMI160_INT_EN_0_ANYMOTION_Z_EN |
BMI160_INT_EN_0_ANYMOTION_Y_EN | BMI160_INT_EN_0_ANYMOTION_X_EN);
prv_read_modify_write(BMI160_REG_INT_EN_0, int_en, int_en);
// configure the anymotion interrupt to fire after 4 successcive
// samples are over the threhold specified
accel_set_shake_sensitivity_high(false /* sensitivity_high */);
prv_write_reg(BMI160_REG_INT_MOTION_0,
0x3 << BMI160_INT_MOTION_1_ANYM_DUR_SHIFT);
// We temporarily latch the interrupt & do not clear it for anymotion interrupts to
// limit the number of anymotion interrupts to 1 per / 1.28 seconds
prv_write_reg(BMI160_REG_INT_LATCH, 0xd);
prv_enable_operating_mode(AccelOperatingModeShakeDetection, BMI160SampleRate_25_HZ);
BMI160_DBG("ACC_CONF = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_ACC_CONF));
}
static void prv_disable_shake_detection(void) {
// don't worry about the configuration registers but disable interrupts
// generated for taps from the accel
uint8_t dis_mask = BMI160_INT_MAP_ANYMOTION_EN_MASK;
prv_read_modify_write(BMI160_REG_INT_MAP_0, 0, dis_mask);
dis_mask = (BMI160_INT_EN_0_ANYMOTION_X_EN | BMI160_INT_EN_0_ANYMOTION_Y_EN |
BMI160_INT_EN_0_ANYMOTION_Z_EN);
prv_read_modify_write(BMI160_REG_INT_EN_0, 0x0, dis_mask);
prv_disable_operating_mode(AccelOperatingModeShakeDetection);
}
static void prv_enable_double_tap_detection(void) {
uint8_t tap_0_cfg =
(0x1 << BMI160_INT_TAP_QUIET_SHIFT) | // 0 = 20 ms, 1 = 30ms
(0x1 << BMI160_INT_TAP_SHOCK_SHIFT) | // 0 = 50 ms, 1 = 75ms
(0x4 << BMI160_INT_TAP_DUR_SHIFT); // 4 = 300 ms
prv_write_reg(BMI160_REG_INT_TAP_0, tap_0_cfg);
// get the current scale
Bmi160Scale scale = prv_get_accel_scale();
// TODO: 4 or 5 bit granularity? - data sheet ambiguous, assume 5
uint32_t threshold = BOARD_CONFIG_ACCEL.accel_config.double_tap_threshold;
const uint32_t STEP = 625; // 62.5 mg step at 2g range
// calculate setting for 2g, then scale to higher g
uint32_t setting = (threshold / STEP) / (scale / 2);
uint8_t tap_1_cfg = (uint8_t)setting;
prv_write_reg(BMI160_REG_INT_TAP_1, tap_1_cfg);
// Enable the double tap detection interrupt
uint8_t en_val = BMI160_INT_MAP_DOUBLE_TAP_EN_MASK;
prv_read_modify_write(BMI160_REG_INT_MAP_0, en_val, en_val);
// Actually enable the double tap interrupt
uint8_t int_en = BMI160_INT_EN_0_D_TAP_EN;
prv_read_modify_write(BMI160_REG_INT_EN_0, int_en, int_en);
BMI160_DBG("ACC_CONF = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_ACC_CONF));
prv_enable_operating_mode(AccelOperatingModeDoubleTapDetection, BMI160SampleRate_200_HZ);
BMI160_DBG("ACC_CONF = 0x%"PRIx8, bmi160_read_reg(BMI160_REG_ACC_CONF));
}
static void prv_disable_double_tap_detection(void) {
uint8_t dis_mask = BMI160_INT_MAP_SINGLE_TAP_EN_MASK | BMI160_INT_MAP_DOUBLE_TAP_EN_MASK;
prv_read_modify_write(BMI160_REG_INT_MAP_0, 0, dis_mask);
dis_mask = BMI160_INT_EN_0_S_TAP_EN | BMI160_INT_EN_0_D_TAP_EN;
prv_read_modify_write(BMI160_REG_INT_EN_0, 0x0, dis_mask);
prv_disable_operating_mode(AccelOperatingModeDoubleTapDetection);
}
void accel_enable_shake_detection(bool on) {
PBL_LOG(LOG_LEVEL_DEBUG, "enable shake detection %d", on);
if (s_shake_detection_enabled == on) {
// the requested change matches what we already have!
return;
}
prv_update_accel_interrupts(on);
if (on) {
prv_enable_shake_detection();
} else {
prv_disable_shake_detection();
}
s_shake_detection_enabled = on;
}
void accel_enable_double_tap_detection(bool on) {
PBL_LOG(LOG_LEVEL_DEBUG, "enable double tap detection %d", on);
if (s_double_tap_detection_enabled == on) {
// the requested change matches what we already have!
return;
}
prv_update_accel_interrupts(on);
if (on) {
prv_enable_double_tap_detection();
} else {
prv_disable_double_tap_detection();
}
s_double_tap_detection_enabled = on;
}
bool accel_get_shake_detection_enabled(void) {
return s_shake_detection_enabled;
}
bool accel_get_double_tap_detection_enabled(void) {
return s_double_tap_detection_enabled;
}
void accel_toggle_power_mode(void) {
static bool enable_low_power = false;
prv_accel_enable_undersampling(enable_low_power);
enable_low_power = !enable_low_power;
}