Sensor Fusion — искусство извлечения правды из шума
Каждый датчик врёт по-своему:
| Датчик | Хорошо | Плохо |
|---|---|---|
| Акселерометр | Статика (где гравитация) | Динамика (вибрации!) |
| Гироскоп | Быстрые изменения | Дрейф со временем |
| Магнитометр | Абсолютный курс | Магнитные помехи |
| GPS | Абсолютная позиция | Медленно, неточно |
Решение: Объединить их умно! 🧠
Измеряет: Линейное ускорение (включая гравитацию!)
В покое: В движении:
↑ g = 9.8 м/с² ↑ g
│ │ ← a (ускорение)
● ●→
🤖 🤖
Из направления $\vec{g}$ можно вычислить Roll и Pitch:
$$ \text{Roll} = \arctan2(a_y, a_z) $$$$ \text{Pitch} = \arctan2(-a_x, \sqrt{a_y^2 + a_z^2}) $$При движении и вибрациях — шум!
Реальный угол: ────────────────────────────
Акселерометр: ─╱╲─╱╲╱╲─╱╲─╱╲╱╲─╱╲─╱╲╱╲─╱╲─
(шум от вибраций)
Измеряет: Угловую скорость (°/сек)
Чтобы получить угол, интегрируем:
$$ \theta(t) = \theta_0 + \int_0^t \omega(\tau) d\tau $$В коде:
angle += gyro_rate * dt;
Даже при $\omega = 0$ датчик выдаёт маленькое значение (bias):
Реальный угол: ────────────────────────────
╱
Гироскоп: ──────────────────────────╱
↗
дрейф (накапливается!)
За 1 минуту может накопиться 10-20° ошибки!
Объединяем лучшее от обоих:
Гироскоп: Высокие частоты (быстрые движения)
│
▼
┌───────────────┐
│ Комплементарный│ ──► Угол
│ фильтр │
└───────────────┘
▲
│
Акселерометр: Низкие частоты (медленные, стабильные)
где:
const float ALPHA = 0.98;
float angle = 0;
void loop() {
float dt = 0.01; // 100 Hz
// Читаем датчики
float gyro_rate = readGyro(); // °/сек
float accel_angle = readAccelAngle(); // ° (из atan2)
// Комплементарный фильтр
angle = ALPHA * (angle + gyro_rate * dt) + (1 - ALPHA) * accel_angle;
Serial.println(angle);
delay(10);
}
Вибрация Дрейф гироскопа
↓ ↓
Акселерометр: ──╱╲╱╲╱╲╱╲──────────────────────
Гироскоп: ─────────────────────────────╱
↗
Комплементарный: ─────────────────────────────
↑ Убрал шум ↑ Убрал дрейф
| Комплементарный | Калман |
|---|---|
| Простой | Сложный |
| 1 параметр (α) | Много параметров |
| Не учитывает шумы | Учитывает статистику шумов |
| Хорош для IMU | Хорош для ЛЮБЫХ датчиков |
Калман — оптимальный по статистике! 📊
Два этапа в каждом цикле:
Predict (Предсказание):
«На основе модели, где я должен быть?»
Update (Коррекция):
«Датчик говорит другое. Кому верить больше?»
где $K_k$ — коэффициент Калмана (сколько верить датчику).
Предсказание: "Думаю, я здесь" ───● (неуверенность ≈)
│
Измерение: "Датчик говорит" ───● (неуверенность ○)
│
▼
Результат: "Скорее всего" ─────● (меньше неуверенности!)
Калман взвешивает предсказание и измерение по их точности.
// Состояние
float angle = 0; // Оценка угла
float bias = 0; // Оценка смещения гироскопа
float P[2][2] = {{1, 0}, {0, 1}}; // Ковариация ошибки
// Параметры (настраиваются!)
float Q_angle = 0.001; // Шум процесса (угол)
float Q_bias = 0.003; // Шум процесса (bias)
float R_measure = 0.03; // Шум измерения акселерометра
float kalmanFilter(float newAngle, float newRate, float dt) {
// Predict
float rate = newRate - bias;
angle += dt * rate;
P[0][0] += dt * (dt * P[1][1] - P[0][1] - P[1][0] + Q_angle);
P[0][1] -= dt * P[1][1];
P[1][0] -= dt * P[1][1];
P[1][1] += Q_bias * dt;
// Update
float S = P[0][0] + R_measure;
float K[2] = {P[0][0] / S, P[1][0] / S};
float y = newAngle - angle; // Innovation
angle += K[0] * y;
bias += K[1] * y;
float P00_temp = P[0][0];
float P01_temp = P[0][1];
P[0][0] -= K[0] * P00_temp;
P[0][1] -= K[0] * P01_temp;
P[1][0] -= K[1] * P00_temp;
P[1][1] -= K[1] * P01_temp;
return angle;
}
Квадрокоптер должен держать горизонт:
Цель: θ = 0°
═══════
╱ ╲
╱ ╲
O O
Реальность:
═══════
╱ ╲ ← наклон!
╱ θ ╲
O O
┌─────────────┐ ┌──────────────┐ ┌───────────┐
│ Акселерометр│────►│ │ │ │
│ (ax,ay,az)│ │ Sensor │ │ PID │
└─────────────┘ │ Fusion │────►│Controller │────► Моторы
┌─────────────┐ │ (Kalman/CF) │ │ │
│ Гироскоп │────►│ │ └───────────┘
│ (gx,gy,gz) │ └──────────────┘
└─────────────┘ │
▼
Roll, Pitch, Yaw
void loop() {
// 1. Читаем IMU
float ax, ay, az, gx, gy, gz;
readIMU(&ax, &ay, &az, &gx, &gy, &gz);
// 2. Sensor Fusion
float accel_roll = atan2(ay, az) * RAD_TO_DEG;
float accel_pitch = atan2(-ax, sqrt(ay*ay + az*az)) * RAD_TO_DEG;
roll = kalmanFilter_roll(accel_roll, gx, dt);
pitch = kalmanFilter_pitch(accel_pitch, gy, dt);
// 3. PID для стабилизации
float roll_correction = computePID(0, roll);
float pitch_correction = computePID(0, pitch);
// 4. Микширование моторов
motor1 = throttle + roll_correction + pitch_correction;
motor2 = throttle - roll_correction + pitch_correction;
motor3 = throttle - roll_correction - pitch_correction;
motor4 = throttle + roll_correction - pitch_correction;
setMotors(motor1, motor2, motor3, motor4);
}
| Метод | Сложность | Качество | Когда использовать |
|---|---|---|---|
| Только гироскоп | ⭐ | Дрейф | Никогда долго |
| Только акселерометр | ⭐ | Шум | Только в статике |
| Комплементарный | ⭐⭐ | Хорошо | IMU, простые проекты |
| Калман 1D | ⭐⭐⭐ | Отлично | IMU, дроны |
| EKF (Extended) | ⭐⭐⭐⭐ | Лучшее | SLAM, автопилоты |
Как робот строит карту и одновременно определяет своё положение!