sl-controls 1e69337ffd feat: Steering PID for differential drive (Issue #616)
Closed-loop yaw-rate controller that converts Jetson Twist.angular.z
to a differential wheel speed offset using IMU gyro Z as feedback.

- include/steering_pid.h + src/steering_pid.c: PID with anti-windup
  (integral clamped to ±200 counts) and rate limiter (10 counts/ms
  max output change) to protect balance PID from sudden steering steps.
  JLINK_TLM_STEERING (0x8A) telemetry at 10 Hz.
- include/mpu6000.h + src/mpu6000.c: expose yaw_rate (board_gz) in
  IMUData so callers have direct bias-corrected gyro Z feedback.
- include/jlink.h + src/jlink.c: add JLINK_TLM_STEERING (0x8A),
  jlink_tlm_steering_t (8 bytes), jlink_send_steering_tlm().
- test/test_steering_pid.c: 78 unit tests (host build with gcc),
  all passing.

Usage (main loop):
  steering_pid_set_target(&s, jlink_state.steer * STEER_OMEGA_SCALE);
  int16_t steer_out = steering_pid_update(&s, imu.yaw_rate, dt);
  motor_driver_update(&motor, balance_cmd, steer_out, now_ms);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:11:05 -04:00

158 lines
5.6 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 "safety.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 = {0}; /* zero-init: guard against icm42688_read no-op on imu_type mismatch */
icm42688_read(&raw);
sum_gx += raw.gx;
sum_gy += raw.gy;
sum_gz += raw.gz;
HAL_Delay(1);
/* Refresh IWDG every 40ms, starting immediately (i=0) — the gap between
* safety_refresh() at the top of the main loop and entry here can be
* ~10ms, so we must refresh on i=0 to avoid the 50ms IWDG window. */
if (i % 40 == 0) safety_refresh();
}
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->yaw_rate = gyro_yaw_rate; /* board_gz: raw bias-corrected gyro Z (Issue #616) */
data->accel_x = ax;
data->accel_z = az;
}