sl-controls 1c95243e52 feat: gyro bias calibration on boot — fixes yaw drift (issues #21, #23)
On boot, before the main loop, sample 1000 gyro readings (~1s) while
board is held still. Compute per-axis mean offset (sensor-frame raw LSBs)
and subtract from all subsequent readings in mpu6000_read().

- mpu6000_calibrate(): LED1+LED2 solid ON during 1s sample window,
  resets filter state to zero once bias is known
- mpu6000_is_calibrated(): gate; main loop blocks arming and USB
  streaming until calibration completes
- Bias subtracted in sensor frame before CW270 axis transform + scale,
  so all three axes (pitch/roll/yaw rate) benefit
- config.h: GYRO_CAL_SAMPLES=1000
- No flash storage — recalibrate fresh each boot (bias varies with temp)

Closes #21 (3.5°/s yaw drift), #23 (gyro bias calibration on boot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:42:34 -05:00

152 lines
5.1 KiB
C
Raw 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.

/*
* mpu6000.c — IMU Sensor Fusion for MPU6000
*
* Wraps the icm42688 SPI driver (which auto-detects MPU6000) and applies
* a complementary filter to produce a stable pitch estimate.
*
* Hardware: MAMBA F722S, MPU6000 on SPI1, CW270 alignment
* Config: Gyro ±2000°/s (init_mpu6000 sets FS_SEL=3)
* Accel ±16g (init_mpu6000 sets AFS_SEL=3)
*/
#include "mpu6000.h"
#include "icm42688.h"
#include "config.h"
#include "stm32f7xx_hal.h"
#include <math.h>
/* Scale factors matching init_mpu6000() config in icm42688.c */
#define GYRO_SCALE (1.0f / 16.384f) /* LSB to °/s — ±2000°/s range */
#define ACCEL_SCALE (1.0f / 2048.0f) /* LSB to g — ±16g range */
/*
* Complementary filter coefficient.
* 0.98 = trust gyro integration, 0.02 = accel correction for drift.
* Tune higher (0.99) for noisier environments.
*/
#define COMP_ALPHA 0.98f
/* Filter state */
static float s_pitch = 0.0f;
static float s_roll = 0.0f;
static float s_yaw = 0.0f;
static uint32_t s_last_tick = 0;
/* Gyro bias offsets (raw sensor LSBs, sensor frame) */
static float s_bias_gx = 0.0f;
static float s_bias_gy = 0.0f;
static float s_bias_gz = 0.0f;
static bool s_calibrated = false;
bool mpu6000_init(void) {
int ret = icm42688_init();
if (ret == 0) {
s_pitch = 0.0f;
s_roll = 0.0f;
s_yaw = 0.0f;
s_last_tick = HAL_GetTick();
}
return (ret == 0);
}
void mpu6000_calibrate(void) {
/* LED1 + LED2 solid ON (active low) — visual indicator: calibrating */
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_RESET);
/*
* Accumulate raw gyro readings in sensor frame.
* Use int32 to avoid float accumulation error over 1000 samples.
* Max raw value ~32768, 1000 samples → max sum ~32.7M < INT32_MAX.
*/
int32_t sum_gx = 0, sum_gy = 0, sum_gz = 0;
for (int i = 0; i < GYRO_CAL_SAMPLES; i++) {
icm42688_data_t raw;
icm42688_read(&raw);
sum_gx += raw.gx;
sum_gy += raw.gy;
sum_gz += raw.gz;
HAL_Delay(1);
}
s_bias_gx = (float)sum_gx / GYRO_CAL_SAMPLES;
s_bias_gy = (float)sum_gy / GYRO_CAL_SAMPLES;
s_bias_gz = (float)sum_gz / GYRO_CAL_SAMPLES;
/* Reset filter state so angles start from zero with clean bias */
s_pitch = 0.0f;
s_roll = 0.0f;
s_yaw = 0.0f;
s_last_tick = HAL_GetTick();
s_calibrated = true;
}
bool mpu6000_is_calibrated(void) {
return s_calibrated;
}
void mpu6000_read(IMUData *data) {
icm42688_data_t raw;
icm42688_read(&raw);
/* Compute dt from wall clock — robust to loop jitter */
uint32_t now = HAL_GetTick();
uint32_t elapsed_ms = now - s_last_tick;
if (elapsed_ms == 0) elapsed_ms = 1; /* min 1ms to avoid divide-by-zero */
if (elapsed_ms > 100) elapsed_ms = 100; /* clamp: don't integrate stale data */
float dt = elapsed_ms * 0.001f;
s_last_tick = now;
/* Convert raw to physical units */
float ax = raw.ax * ACCEL_SCALE; /* g */
float ay = raw.ay * ACCEL_SCALE; /* g */
float az = raw.az * ACCEL_SCALE; /* g */
/*
* CW270 alignment transform (Betaflight convention, R_CW270):
*
* R = [[0, 1, 0], [-1, 0, 0], [0, 0, 1]]
* board_forward = sensor_Y → pitch accel uses ay
* board_right = -sensor_X → roll accel uses -ax
* board_gx (roll rate) = sensor_gy (board_gx = R[0,·]·omega_sensor)
* board_gy (pitch rate) = -sensor_gx (board_gy = R[1,·]·omega_sensor)
* board_gz (yaw rate) = sensor_gz (unchanged)
*
* Convention: pitch+ = nose up, roll+ = right bank, yaw+ = CW from above.
*/
/* Subtract bias (sensor-frame raw LSBs) before axis transform + scale */
float gyro_pitch_rate = -(raw.gx - s_bias_gx) * GYRO_SCALE; /* °/s board_gy = -sensor_gx */
float gyro_roll_rate = (raw.gy - s_bias_gy) * GYRO_SCALE; /* °/s board_gx = sensor_gy */
float gyro_yaw_rate = (raw.gz - s_bias_gz) * GYRO_SCALE; /* °/s unchanged */
/*
* Accel-derived angles after CW270 transform.
* board_ax (forward) = sensor_ay → pitch = atan2(ay, az)
* board_ay (right) = -sensor_ax → roll = atan2(-ax, az)
* Valid while total accel ≈ 1g (low linear acceleration).
*/
float accel_pitch = atan2f( ay, az) * (180.0f / 3.14159265358979f);
float accel_roll = atan2f(-ax, az) * (180.0f / 3.14159265358979f);
/*
* Complementary filter for pitch and roll:
* angle = α * (angle + ω*dt) + (1α) * accel_angle
* Gyro integration tracks fast dynamics; accel corrects drift.
*/
s_pitch = COMP_ALPHA * (s_pitch + gyro_pitch_rate * dt)
+ (1.0f - COMP_ALPHA) * accel_pitch;
s_roll = COMP_ALPHA * (s_roll + gyro_roll_rate * dt)
+ (1.0f - COMP_ALPHA) * accel_roll;
/* Yaw: pure gyro integration — no accel correction, drifts over time */
s_yaw += gyro_yaw_rate * dt;
data->pitch = s_pitch;
data->pitch_rate = gyro_pitch_rate;
data->roll = s_roll;
data->yaw = s_yaw;
data->accel_x = ax;
data->accel_z = az;
}