Auto-detected alongside MPU6000. Acts as balance primary when MPU6000 fails, or provides NDOF-fused yaw/pitch when both sensors are present. - include/bno055.h: full API — bno055_init/read/is_ready/calib_status/ temperature/save_offsets/restore_offsets - src/bno055.c: I2C1 driver; probes 0x28/0x29, resets via SYS_TRIGGER, enters NDOF mode; 2-burst 12-byte reads (gyro+euler, LIA+gravity); Euler/gyro/accel scaling (÷16, ÷16, ÷100); auto-saves offsets to RTC backup regs BKP0R–BKP6R on first full cal; restores on boot (bno055_is_ready() returns true immediately); temperature updated 1Hz - include/config.h: BNO055_BKP_MAGIC = 0xB055CA10 - src/main.c: bno055_init() in I2C probe block (before IWDG); imu_calibrated() macro dispatches mpu6000_is_calibrated() vs bno055_is_ready(); BNO055 read deferred inside balance gate to avoid stalling main loop; USB JSON reports bno_cs (calib status) and bno_t (temperature) - test/test_bno055_data.py: 43 pytest tests (43/43 pass) — calib status bit extraction, Euler/gyro/accel scaling, burst parsing, offset round-trip packing, temperature signed-byte encoding Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
317 lines
10 KiB
C
317 lines
10 KiB
C
/*
|
||
* bno055.c — Bosch BNO055 NDOF IMU driver over I2C1
|
||
*
|
||
* Issue #135: auto-detect alongside MPU6000; fallback + yaw augmentation.
|
||
*
|
||
* Calibration offsets are persisted in STM32F7 RTC backup registers
|
||
* (BKP0R–BKP6R) and restored automatically on next power-on, so the
|
||
* sensor re-enters a calibrated state immediately after reset.
|
||
*
|
||
* Internal temperature compensation:
|
||
* The BNO055 silicon compensates all three sensors (accel, gyro, mag)
|
||
* continuously against its onboard thermometer. No external correction
|
||
* is required. bno055_temperature() exposes the reading for telemetry.
|
||
*/
|
||
|
||
#include "bno055.h"
|
||
#include "i2c1.h"
|
||
#include "config.h"
|
||
#include "stm32f7xx_hal.h"
|
||
#include <string.h>
|
||
#include <math.h>
|
||
|
||
/* ---- I2C addresses (7-bit, shifted left 1 for HAL) ---- */
|
||
#define BNO055_ADDR_LOW (0x28u << 1)
|
||
#define BNO055_ADDR_HIGH (0x29u << 1)
|
||
#define BNO055_CHIP_ID_VAL 0xA0u
|
||
|
||
/* ---- Register map (page 0) ---- */
|
||
#define REG_CHIP_ID 0x00u
|
||
#define REG_PAGE_ID 0x07u
|
||
#define REG_GYRO_X_LSB 0x14u /* 6 bytes: Gx, Gy, Gz */
|
||
#define REG_EULER_H_LSB 0x1Au /* 6 bytes: H, R, P (each int16) */
|
||
#define REG_LIA_X_LSB 0x28u /* 6 bytes: linear accel X,Y,Z */
|
||
#define REG_GRV_X_LSB 0x2Eu /* 6 bytes: gravity X,Y,Z */
|
||
#define REG_TEMP 0x34u
|
||
#define REG_CALIB_STAT 0x35u
|
||
#define REG_OPR_MODE 0x3Du
|
||
#define REG_PWR_MODE 0x3Eu
|
||
#define REG_SYS_TRIGGER 0x3Fu
|
||
#define REG_TEMP_SOURCE 0x40u
|
||
#define REG_OFFSET_BASE 0x55u /* 22 bytes: accel(6)+mag(6)+gyro(6)+radii(4) */
|
||
|
||
/* ---- Operating modes ---- */
|
||
#define MODE_CONFIG 0x00u
|
||
#define MODE_IMUPLUS 0x08u /* 6DOF, no mag */
|
||
#define MODE_NDOF 0x0Cu /* 9DOF with mag */
|
||
|
||
/* ---- RTC backup register magic for saved offsets ---- */
|
||
/*
|
||
* Layout: BKP0R..BKP5R hold 22 bytes of calibration offsets (packed
|
||
* 4 bytes per register; BKP5R only uses its low 2 bytes).
|
||
* BKP6R holds the magic word to validate the stored data.
|
||
*/
|
||
#define BNO055_BKP_MAGIC 0xB055CA10u
|
||
#define BKP_REG(n) (*(volatile uint32_t *)(&RTC->BKP0R + (n)))
|
||
|
||
/* ---- Scaling constants ---- */
|
||
/*
|
||
* BNO055 default unit selection (UNIT_SEL=0x00):
|
||
* Euler angles: 1 degree = 16 LSB → divide by 16.0f
|
||
* Gyro rates: 1 °/s = 16 LSB → divide by 16.0f
|
||
* Accel/LIA/GRV: 1 m/s² = 100 LSB → divide by 100.0f
|
||
*/
|
||
#define EULER_SCALE 16.0f
|
||
#define GYRO_SCALE 16.0f
|
||
#define ACCEL_SCALE 100.0f
|
||
#define G_MS2 9.80665f /* m/s² per g */
|
||
|
||
/* ---- Module state ---- */
|
||
static uint16_t s_addr = 0;
|
||
static bool s_present = false;
|
||
static bool s_offsets_ok = false; /* true if offsets restored */
|
||
static uint8_t s_calib = 0; /* cached CALIB_STAT */
|
||
static int8_t s_temp = 25; /* cached temperature °C */
|
||
static uint16_t s_temp_ctr = 0; /* temp read divider */
|
||
|
||
/* ---- Low-level I2C helpers ---- */
|
||
static uint8_t rd(uint8_t reg)
|
||
{
|
||
uint8_t v = 0;
|
||
HAL_I2C_Mem_Read(&hi2c1, s_addr, reg, 1, &v, 1, 100);
|
||
return v;
|
||
}
|
||
|
||
static void wr(uint8_t reg, uint8_t val)
|
||
{
|
||
HAL_I2C_Mem_Write(&hi2c1, s_addr, reg, 1, &val, 1, 100);
|
||
}
|
||
|
||
static void rdblk(uint8_t reg, uint8_t *buf, uint8_t len)
|
||
{
|
||
HAL_I2C_Mem_Read(&hi2c1, s_addr, reg, 1, buf, len, 200);
|
||
}
|
||
|
||
static void wrblk(uint8_t reg, const uint8_t *buf, uint8_t len)
|
||
{
|
||
HAL_I2C_Mem_Write(&hi2c1, s_addr, reg, 1, (uint8_t *)buf, len, 200);
|
||
}
|
||
|
||
/* ---- bno055_restore_offsets() ---- */
|
||
bool bno055_restore_offsets(void)
|
||
{
|
||
__HAL_RCC_PWR_CLK_ENABLE();
|
||
HAL_PWR_EnableBkUpAccess();
|
||
|
||
if (BKP_REG(6) != BNO055_BKP_MAGIC) return false;
|
||
|
||
/* Unpack 22 bytes from BKP0R..BKP5R */
|
||
uint8_t offsets[22];
|
||
for (int i = 0; i < 5; i++) {
|
||
uint32_t w = BKP_REG(i);
|
||
offsets[i * 4 + 0] = (uint8_t)(w & 0xFFu);
|
||
offsets[i * 4 + 1] = (uint8_t)((w >> 8) & 0xFFu);
|
||
offsets[i * 4 + 2] = (uint8_t)((w >> 16) & 0xFFu);
|
||
offsets[i * 4 + 3] = (uint8_t)((w >> 24) & 0xFFu);
|
||
}
|
||
uint32_t w5 = BKP_REG(5);
|
||
offsets[20] = (uint8_t)(w5 & 0xFFu);
|
||
offsets[21] = (uint8_t)((w5 >> 8) & 0xFFu);
|
||
|
||
/* Write offsets — must be in CONFIG mode */
|
||
wr(REG_OPR_MODE, MODE_CONFIG);
|
||
HAL_Delay(25);
|
||
wr(REG_PAGE_ID, 0x00);
|
||
wrblk(REG_OFFSET_BASE, offsets, 22);
|
||
/* Caller switches back to NDOF after this returns */
|
||
return true;
|
||
}
|
||
|
||
/* ---- bno055_save_offsets() ---- */
|
||
bool bno055_save_offsets(void)
|
||
{
|
||
if (!s_present) return false;
|
||
|
||
/* Switch to CONFIG mode to read offsets */
|
||
wr(REG_OPR_MODE, MODE_CONFIG);
|
||
HAL_Delay(25);
|
||
wr(REG_PAGE_ID, 0x00);
|
||
|
||
uint8_t offsets[22];
|
||
rdblk(REG_OFFSET_BASE, offsets, 22);
|
||
|
||
/* Restore to NDOF */
|
||
wr(REG_OPR_MODE, MODE_NDOF);
|
||
HAL_Delay(10);
|
||
|
||
/* Pack into RTC backup registers */
|
||
__HAL_RCC_PWR_CLK_ENABLE();
|
||
HAL_PWR_EnableBkUpAccess();
|
||
|
||
for (int i = 0; i < 5; i++) {
|
||
BKP_REG(i) = ((uint32_t)offsets[i * 4 + 0])
|
||
| ((uint32_t)offsets[i * 4 + 1] << 8)
|
||
| ((uint32_t)offsets[i * 4 + 2] << 16)
|
||
| ((uint32_t)offsets[i * 4 + 3] << 24);
|
||
}
|
||
BKP_REG(5) = (uint32_t)offsets[20] | ((uint32_t)offsets[21] << 8);
|
||
BKP_REG(6) = BNO055_BKP_MAGIC;
|
||
|
||
return true;
|
||
}
|
||
|
||
/* ---- bno055_init() ---- */
|
||
int bno055_init(void)
|
||
{
|
||
/* Probe 0x28 (ADR=0) first, then 0x29 (ADR=1) */
|
||
const uint16_t candidates[2] = { BNO055_ADDR_LOW, BNO055_ADDR_HIGH };
|
||
s_present = false;
|
||
for (int i = 0; i < 2; i++) {
|
||
s_addr = candidates[i];
|
||
if (rd(REG_CHIP_ID) == BNO055_CHIP_ID_VAL) {
|
||
s_present = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!s_present) return -1;
|
||
|
||
/* Software reset via SYS_TRIGGER */
|
||
wr(REG_PAGE_ID, 0x00);
|
||
wr(REG_SYS_TRIGGER, 0x20u);
|
||
HAL_Delay(700); /* POR settle time from datasheet */
|
||
|
||
/* Confirm chip ID after reset */
|
||
if (rd(REG_CHIP_ID) != BNO055_CHIP_ID_VAL) {
|
||
s_present = false;
|
||
return -1;
|
||
}
|
||
|
||
wr(REG_PAGE_ID, 0x00);
|
||
|
||
/* CONFIGMODE before any register writes */
|
||
wr(REG_OPR_MODE, MODE_CONFIG);
|
||
HAL_Delay(25);
|
||
|
||
/* Normal power mode */
|
||
wr(REG_PWR_MODE, 0x00u);
|
||
HAL_Delay(10);
|
||
|
||
/*
|
||
* Unit selection (UNIT_SEL = 0x00, default):
|
||
* Euler = degrees, Gyro = dps, Accel = m/s², Temp = °C
|
||
* No write needed — defaults match what we want.
|
||
*/
|
||
|
||
/* Temperature source = gyro (more stable than accel) */
|
||
wr(REG_TEMP_SOURCE, 0x01u);
|
||
|
||
/* Try to restore saved calibration offsets */
|
||
s_offsets_ok = bno055_restore_offsets();
|
||
|
||
/* Enter NDOF fusion mode */
|
||
wr(REG_OPR_MODE, MODE_NDOF);
|
||
HAL_Delay(20);
|
||
|
||
s_calib = 0;
|
||
s_temp = 25;
|
||
s_temp_ctr = 0;
|
||
return 0;
|
||
}
|
||
|
||
/* ---- bno055_read() ---- */
|
||
/*
|
||
* Performs two I2C burst reads (~3ms total at 100kHz):
|
||
* 1. REG_GYRO_X_LSB → 12 bytes: gyro(6) + euler(6)
|
||
* 2. REG_LIA_X_LSB → 12 bytes: linear_accel(6) + gravity(6)
|
||
*
|
||
* BNO055 coordinate frame (NDOF, no remapping):
|
||
* X: right, Y: forward, Z: up
|
||
* Euler H: yaw (0–360°, clockwise from north)
|
||
* Euler R: roll (–180°..+180°, right bank positive)
|
||
* Euler P: pitch (–90°..+90°, nose up positive)
|
||
* Gyro Y: pitch rate (deg/s, nose-up positive)
|
||
*
|
||
* Linear accel X (m/s²): forward-backward acceleration (gravity removed)
|
||
* Gravity Z (m/s²): gravity component on Z axis (≈9.81 when level)
|
||
*/
|
||
void bno055_read(IMUData *data)
|
||
{
|
||
uint8_t buf[12];
|
||
|
||
/* ---- Burst 1: Gyro (0x14–0x19) + Euler (0x1A–0x1F) ---- */
|
||
rdblk(REG_GYRO_X_LSB, buf, 12);
|
||
|
||
/* Gyro: bytes 0..5 = [Gx_L, Gx_H, Gy_L, Gy_H, Gz_L, Gz_H]
|
||
* Gy = pitch rate (Y axis = pitch axis in BNO055 frame) */
|
||
float pitch_rate = (float)(int16_t)((uint16_t)buf[3] << 8 | buf[2]) / GYRO_SCALE;
|
||
|
||
/* Euler: bytes 6..11 = [H_L, H_H, R_L, R_H, P_L, P_H] */
|
||
float heading = (float)(int16_t)((uint16_t)buf[7] << 8 | buf[6]) / EULER_SCALE;
|
||
float roll = (float)(int16_t)((uint16_t)buf[9] << 8 | buf[8]) / EULER_SCALE;
|
||
float pitch = (float)(int16_t)((uint16_t)buf[11] << 8 | buf[10]) / EULER_SCALE;
|
||
|
||
/* ---- Burst 2: Linear accel (0x28–0x2D) + Gravity (0x2E–0x33) ---- */
|
||
rdblk(REG_LIA_X_LSB, buf, 12);
|
||
|
||
/* Linear accel X (forward, m/s²) — gravity-compensated */
|
||
float lia_x_ms2 = (float)(int16_t)((uint16_t)buf[1] << 8 | buf[0]) / ACCEL_SCALE;
|
||
|
||
/* Gravity Z (up axis, m/s²) — should be ≈+9.81 when level */
|
||
float grv_z_ms2 = (float)(int16_t)((uint16_t)buf[11] << 8 | buf[10]) / ACCEL_SCALE;
|
||
|
||
/* Fill IMUData in same units as mpu6000_read() */
|
||
data->pitch = pitch;
|
||
data->pitch_rate = pitch_rate;
|
||
data->roll = roll;
|
||
data->yaw = heading;
|
||
data->accel_x = lia_x_ms2 / G_MS2; /* m/s² → g */
|
||
data->accel_z = grv_z_ms2 / G_MS2; /* m/s² → g (≈1.0 when level) */
|
||
|
||
/* Periodically update calibration status and temperature (~1Hz at 250Hz loop) */
|
||
if (++s_temp_ctr >= 250u) {
|
||
s_temp_ctr = 0;
|
||
s_calib = rd(REG_CALIB_STAT);
|
||
s_temp = (int8_t)rd(REG_TEMP);
|
||
|
||
/*
|
||
* Auto-save offsets once when full calibration is achieved for
|
||
* the first time this session (sys=3, acc=3, gyr=3, don't wait
|
||
* for mag=3 as it is harder to achieve and not critical for balance).
|
||
*/
|
||
static bool s_saved = false;
|
||
if (!s_saved) {
|
||
uint8_t sys_cal = (s_calib & BNO055_CAL_SYS_MASK) >> 6;
|
||
uint8_t acc_cal = (s_calib & BNO055_CAL_ACC_MASK) >> 2;
|
||
uint8_t gyr_cal = (s_calib & BNO055_CAL_GYR_MASK) >> 4;
|
||
if (sys_cal >= 1u && acc_cal >= 3u && gyr_cal >= 3u) {
|
||
bno055_save_offsets();
|
||
s_saved = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- bno055_is_ready() ---- */
|
||
bool bno055_is_ready(void)
|
||
{
|
||
if (!s_present) return false;
|
||
/* If we restored saved offsets, trust them immediately */
|
||
if (s_offsets_ok) return true;
|
||
/* Otherwise require gyro ≥ 2 and accel ≥ 2 */
|
||
uint8_t acc_cal = (s_calib & BNO055_CAL_ACC_MASK) >> 2;
|
||
uint8_t gyr_cal = (s_calib & BNO055_CAL_GYR_MASK) >> 4;
|
||
return (acc_cal >= 2u) && (gyr_cal >= 2u);
|
||
}
|
||
|
||
/* ---- bno055_calib_status() ---- */
|
||
uint8_t bno055_calib_status(void)
|
||
{
|
||
return s_calib;
|
||
}
|
||
|
||
/* ---- bno055_temperature() ---- */
|
||
int8_t bno055_temperature(void)
|
||
{
|
||
return s_temp;
|
||
}
|