Three bugs prevented mpu6000_is_calibrated() from returning true,
blocking arming and balance mode:
1. WHO_AM_I single-attempt: one SPI glitch returning 0x00 caused
icm42688_init() to return -128, skipping mpu6000_calibrate()
entirely. Fix: retry WHO_AM_I up to 3 times with 10ms gaps.
2. icm42688_read() rx[15] uninitialized: if HAL_SPI_TransmitReceive()
failed, garbage stack data was accumulated as gyro bias. Fix: zero-
init rx[15] so failed transfers produce zero data.
3. mpu6000_calibrate() raw uninitialized: UB if icm42688_read() is
a no-op (imu_type mismatch). Fix: zero-init raw each iteration.
Also add SCB_InvalidateDCache_by_Addr() on SPI rx buffers in rreg()
and icm42688_read() for DCache coherency. Currently a no-op (DCache
is not enabled), but required if SCB_EnableDCache() is added — stack
buffers in SRAM2 are in the cacheable memory region on STM32F7.
Fix misleading DCache comment in icm42688.c (claimed DCache was
disabled by main.c; actually SCB_EnableDCache() is never called).
Build: 59904 bytes Flash (+512), 17100 bytes RAM — SUCCESS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
i%40==39 fired the first IWDG refresh only after 40ms of calibration.
Combined with ~10ms of main loop overhead before entering calibrate(),
total elapsed since last refresh could exceed the 50ms IWDG window.
Change to i%40==0: first refresh fires at i=0 (<1ms after entry),
subsequent refreshes every 40ms — safely within the 50ms window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 'G' CDC command that disarms and re-runs gyro bias calibration.
safety_refresh() added to calibration loop (every 40ms) so IWDG
does not trip during the 1s blocking re-cal when watchdog is running.
GYRO CAL button in ui/index.html sends 'G' and shows status feedback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The MAMBA F722S mounts MPU6000 at CW270 (clockwise 270°) which applies
rotation matrix R = [[0,1,0],[-1,0,0],[0,0,1]] to transform sensor axes
to board axes (Betaflight convention).
Firmware (mpu6000.c):
- accel_pitch: was atan2(ax, az) → now atan2(ay, az)
board_forward = sensor_Y, so ay drives pitch not ax
- accel_roll: was atan2(ay, az) → now atan2(-ax, az)
board_right = -sensor_X, so -ax drives roll not ay
- gyro_pitch_rate: was +raw.gx → now -raw.gx
board_gy (pitch) = -sensor_gx after R_CW270 transform
- gyro_roll_rate: raw.gy unchanged (board_gx = sensor_gy ✓)
- gyro_yaw_rate: raw.gz unchanged ✓
UI (index.html) rotation sign fixes:
- roll → -rotation.z: Three.js +z = CCW from camera = left bank;
our convention is right-bank-positive so negate
- yaw → -rotation.y: Three.js +y = CCW from above; sensor_Z points
down on MAMBA (az ≈ +1g when level) so gz+ = CW physical; negate
- pitch → +rotation.x: correct as-is (Three.js +x tilts nose up ✓)
Closes#15.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue #12 — Roll displayed as pitch:
- Firmware was sending r=pitch_rate (wrong). Changed to r=roll_deg*10.
- mpu6000.c: add roll complementary filter (accel atan2(ay,az) +
gyro gy integration, same COMP_ALPHA=0.98 as pitch).
- IMUData: add roll and yaw fields.
- UI: updateIMU() now uses data.r/10 for roll (not client-side filter
that computed from ax/ay/az which firmware never sent).
- Three.js: roll -> rotation.z (banking), pitch -> rotation.x (tipping)
— axes were already correct, fix was the firmware data.
Issue #13 — Add yaw telemetry:
- Firmware: gyro Z integration (gz * dt) → s_yaw, sent as y=yaw_deg*10.
Gyro-only, drifts — no magnetometer.
- IMUData: yaw field added.
- UI: yaw -> rotation.y (spinning on vertical axis). Displayed in HUD.
- UI: YAW RESET button captures current yaw as new zero reference
(client-side offset, no new firmware command needed).
Closes#12, #13.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>