sl-firmware 87b45e1b97 feat(firmware): BNO055 NDOF IMU driver on I2C1 (Issue #135)
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>
2026-03-02 09:40:18 -05:00

317 lines
10 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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
* (BKP0RBKP6R) 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 (0360°, 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 (0x140x19) + Euler (0x1A0x1F) ---- */
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 (0x280x2D) + Gravity (0x2E0x33) ---- */
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;
}