# Методическое пособие: цифровая лаборатория на базе MPU6050

Стенд: ESP32 + `Mpu6050Driver` (FreeRTOS, 200 Гц) + вывод в Serial Plotter / Node-RED / Grafana. Пособие связывает показания реального датчика с формулами из школьной программы — физика, геометрия, математика.

## Кто чем пользуется

1. **Инженер** отвечает за железо: калибровку, шаг интегратора $\Delta t = 5\,\text{мс}$, устойчивую трансляцию данных в Serial-порт.
2. **Учитель** проводит эксперимент по сценарию ниже и обсуждает график с классом.
3. **Методист** привязывает конкретные параметры датчика к темам учебной программы.

---

## Модуль 1. Механика и динамика

### Урок 1. Невесомость и перегрузка

* Тема: силы в механике, вес тела, невесомость, перегрузка.
* Параметры датчика: `TotalG`, `Z-Acc`, `Status`.

**Ход работы.** Датчик неподвижно лежит на столе — на графике `TotalG` прямая линия `1.00 g`. Вопрос классу: «Что именно измеряет датчик — силу тяжести или что-то другое?» Затем ученик роняет датчик с высоты ~1.5 м на мягкий ложемент (поролон).

**На графике:** `TotalG` мгновенно падает с `1.00 g` до `~0.00 g` на время полёта (~0.5 с), затем резкий всплеск до `2.5–3.0 g` в момент приземления.

```
 TotalG (g)
  3.00 │                                     ▲ [Удар / Перегрузка]
  2.00 │                                    ╱ ╲
  1.00 │ ──────────────────┐               ╱   ╲
  0.00 │                   └──────────────┘     └───────────► Время (t)
                           [Невесомость: 0G]
```

**Разбор.** Вес тела — это сила, с которой тело давит на опору или подвес, а не сама гравитация. При свободном падении ускорение прибора $a = g$, и по второму закону Ньютона:

$$P = m(g - a) \implies P = m(g - g) = 0$$

Датчик не фиксирует деформацию своих внутренних MEMS-элементов — отсюда `TotalG ≈ 0`. Пиковый всплеск при ударе иллюстрирует обратный случай: $P = m(g + a)$.

**Визуализация.** Вместо ASCII-графика выше запустите `tools/g_force_view.py` —
он рисует ту же ленту `TotalG` в реальном времени с подсветкой невесомости и удара,
и крупной текстовой подписью на весь экран (удобно проецировать на класс):
```bash
cd tools && pip install -r requirements.txt && python3 g_force_view.py <порт>
```

### Урок 2. Маятник и затухающие колебания

* Тема: механические колебания, период и частота, закон сохранения энергии.
* Параметры датчика: `Pitch`, `Roll`, `TotalG`.

**Ход работы.** Датчик закреплён на нити длиной $L = 1\,\text{м}$ — математический маятник. Отклони на ~30° и отпусти.

**На графике:** `Pitch` (или `Roll`, в зависимости от ориентации платы) рисует затухающую синусоиду; `TotalG` колеблется с удвоенной частотой, с пиками ровно в моменты прохождения нижней точки.

```
 Pitch (°)                                 TotalG (g)
  +30 │    ▲         ▲                      1.20 │    ▲    ▲    ▲    ▲
    0 │ ──╱─╲───────╱─╲──────► Время (t)    1.00 │ ──╱─╲──╱─╲──╱─╲──╱─╲──► Время (t)
  -30 │      ▼         ▼                         │ [Нижняя точка равновесия]
```

**Разбор.**
1. По временным меткам двух соседних пиков `Pitch` ученики находят экспериментальный период $T$.
2. Сверяют с формулой Гюйгенса: $T = 2\pi\sqrt{L/g} \approx 2.006\,\text{с}$ для $L = 1\,\text{м}$, и считают погрешность измерения.
3. В крайних точках отклонения скорость маятника $v = 0$ — перегрузка минимальна. В нижней точке $v = v_{max}$, и добавляется центростремительная сила:
   $$F_{\text{опоры}} = m\left(g + \frac{v^2}{L}\right)$$
   Поэтому `TotalG > 1.00 g` ровно дважды за период.

**Визуализация.** `tools/orientation_cube.py` рисует 3D-куб, который качается
синхронно с реальным маятником — тот же инструмент пригодится в Уроках 3 и 4:
```bash
cd tools && pip install -r requirements.txt && python3 orientation_cube.py <порт>
```

---

## Модуль 2. Геометрия и тригонометрия

### Урок 3. Проекции вектора и тригонометрические функции угла

* Тема: синус, косинус, тангенс прямоугольного треугольника; проекции векторов.
* Параметры: сырые `ax`, `ay`, `az` (в g) и расчётные `Pitch`/`Roll`.

**Ход работы.** Датчик закреплён на линейке, которая медленно поднимается от горизонтали к вертикали — линейка выступает гипотенузой прямоугольного треугольника.

```
      /|
     / |  az = g·cos(θ)
    /  |
   /───┘  ax = -g·sin(θ)
  / θ (угол Pitch, вычисляется как atan2)
 └───────────────────────
```

**Ход работы.** Выставь линейку под углами $0°, 30°, 45°, 60°, 90°$ по транспортиру, зафиксируй `ax`/`az` в каждой точке.

| Угол $\theta$ | $a_x$ (эксп.) | $\sin\theta$ (теор.) | $a_z$ (эксп.) | $\cos\theta$ (теор.) |
|---|---|---|---|---|
| 0° | 0.00 | 0.000 | 1.00 | 1.000 |
| 30° | -0.50 | -0.500 | 0.86 | 0.866 |
| 90° | -1.00 | -1.000 | 0.00 | 0.000 |

**Разбор.** При повороте платы вокруг оси Y: $a_x = -\sin\theta$, $a_z = \cos\theta$. Отсюда формула, заложенная в драйвер:
$$\theta = \arctan\left(\frac{\sin\theta}{\cos\theta}\right) \implies \text{Pitch} = \operatorname{atan2}(-a_x,\, a_z)$$

### Урок 4. Проблема Gimbal Lock

* Тема: ограничения углов Эйлера, геометрия пространственных поворотов.
* Параметры: `Pitch`, `Roll`.

**Ход работы.** Медленно поднимай датчик вертикально, пока `Pitch` не подойдёт к `+90°`. В этой точке слегка покачай плату.

**Наблюдение.** `Roll` начинает хаотично «прыгать» при малейшем сдвиге, хотя физически плата почти не двигалась.

**Разбор.** Вблизи $\pm90°$ по тангажу ось крена (Roll) и ось курса (Yaw) физически совпадают — небольшая неопределённость измерения даёт огромный скачок вычисленного угла. Это и есть Gimbal Lock — классическая причина, по которой в авиации и космонавтике для ориентации используют кватернионы, а не углы Эйлера напрямую.

**Визуализация.** Этот урок особенно хорошо виден на `tools/orientation_cube.py`
(запуск — см. конец Урока 2): при Pitch вблизи ±90° куб на экране визуально
"складывается" — оси Roll и Yaw сливаются в одну линию так же, как это описано
выше, а скрипт выводит предупреждение "близко к Gimbal Lock" прямо на графике.

---

## Модуль 3. Вычислительная математика и статистика

### Урок 5. Численное интегрирование и накопление погрешности (дрейф Yaw)

* Тема: интеграл как сумма, погрешность измерений, случайные величины.
* Параметры: `Yaw`, сырые и калиброванные значения гироскопа `gz`.

**Ход работы.** Датчик жёстко закреплён на столе и абсолютно неподвижен. Если временно отключить калибровку (или ввести искусственное смещение), на графике `Yaw` виден непрерывный линейный рост угла, хотя плата не двигалась.

```
 Yaw (°)
  10.0 │                                             ▲ [Линейный дрейф угла]
   5.0 │                                     ╱╱╱╱╱╱╱╱
   0.0 │ ────────────────────────────╱╱╱╱╱╱╱╱────────────────► Время (t)
         [Датчик строго неподвижен на столе]
```

**Разбор.** Гироскоп выдаёт угловую скорость $\omega = d\theta/dt$; угол получаем интегрированием:
$$\theta(t) = \int_0^t \omega(t)\,dt \approx \sum_{i=1}^n \omega_i \cdot \Delta t$$
Каждый шаг задачи FreeRTOS — ровно $\Delta t = 0.005\,\text{с}$. Если из-за отсутствия калибровки остаётся систематическая ошибка нуля $\epsilon = 2°/\text{с}$, за секунду угол уплывёт на $2°$, а за 3 минуты — на полный оборот $360°$ в полной статике.

**Практическое задание.** Собрать 1000 значений сырого шума неподвижного гироскопа, построить гистограмму (Excel/Google Таблицы), найти математическое ожидание (систематическую ошибку) и СКО (случайный шум). Это объясняет, зачем нужна `calibrateGyro()` — она находит именно систематическую составляющую и вычитает её из каждого кадра.

**Визуализация.** `tools/yaw_drift_view.py` рисует накопленное отклонение `Yaw`
от стартового значения в реальном времени — на неподвижном откалиброванном
датчике линия должна остаться почти горизонтальной; линейный рост на графике
и есть дрейф, о котором говорится выше:
```bash
cd tools && pip install -r requirements.txt && python3 yaw_drift_view.py <порт>
```

### Урок 6. Центростремительное ускорение

* Тема: равномерное движение по окружности.
* Параметры: линейное ускорение по осям (`Z-Acc` или соответствующая ось), угловая скорость гироскопа.

**Ход работы.** Вращай датчик на нити фиксированной длины $R$ с разной скоростью.

**Разбор.** Проверка формулы центростремительного ускорения:
$$a_c = \omega^2 \cdot R$$
Ученики берут радиус нити, снимают угловую скорость с гироскопа и сверяют теоретическое $a_c$ с пиком ускорения на графике акселерометра.

---

## Приложение: профиль прошивки для переключения уроков

Чтобы не пересобирать прошивку между уроками, можно временно заменить `src/main.cpp` на версию с переключением режимов по команде из терминала (`1`/`2`/`3`). Приоритет задачи и структура повторяют боевой `main.cpp` этого компонента — при обновлении реального `main.cpp` синхронизируй и этот пример.

```cpp
#include <Arduino.h>
#include "Mpu6050Driver.h"

#define I2C_SDA 21
#define I2C_SCL 22
#define LED_PIN 2

Mpu6050Driver imu(Wire);
TaskHandle_t imuTaskHandle = NULL;
Mpu6050Driver::Data sharedData;
portMUX_TYPE dataMutex = portMUX_INITIALIZER_UNLOCKED;

enum LabMode { PHYSICS_1_2, GEOMETRY_3, MATH_4 };
LabMode currentMode = PHYSICS_1_2;

void vImuTask(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(5); // 200 Hz
    const float dt = 0.005f;

    for (;;) {
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
        imu.update(dt);

        portENTER_CRITICAL(&dataMutex);
        sharedData = imu.getData();
        portEXIT_CRITICAL(&dataMutex);
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);

    if (!imu.begin(I2C_SDA, I2C_SCL)) {
        while (1) { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); delay(50); }
    }

    imu.calibrateGyro(500);
    imu.initAnglesFromAccel();

    // Приоритет 3: выше loop() (1), заметно ниже системных задач Wi-Fi/BT (~18-23).
    xTaskCreatePinnedToCore(vImuTask, "IMU_Task", 4096, NULL, 3, &imuTaskHandle, 0);
}

void loop() {
    if (Serial.available() > 0) {
        char cmd = Serial.read();
        if (cmd == '1') { currentMode = PHYSICS_1_2; Serial.println(F("\n>>> РЕЖИМ ФИЗИКИ")); }
        if (cmd == '2') { currentMode = GEOMETRY_3;  Serial.println(F("\n>>> РЕЖИМ ГЕОМЕТРИИ")); }
        if (cmd == '3') { currentMode = MATH_4;      Serial.println(F("\n>>> РЕЖИМ ВЫЧИСЛИТЕЛЬНОЙ МАТЕМАТИКИ")); }
    }

    Mpu6050Driver::Data d;
    portENTER_CRITICAL(&dataMutex);
    d = sharedData;
    portEXIT_CRITICAL(&dataMutex);

    float totalAccel = sqrt(d.ax * d.ax + d.ay * d.ay + d.az * d.az);

    switch (currentMode) {
        case PHYSICS_1_2:
            Serial.print(F("Pitch:"));   Serial.print(d.pitch, 2);
            Serial.print(F(",TotalG:")); Serial.println(totalAccel, 3);
            break;
        case GEOMETRY_3:
            Serial.print(F("ax_Raw_g:")); Serial.print(d.ax, 4);
            Serial.print(F(",az_Raw_g:")); Serial.print(d.az, 4);
            Serial.print(F(",Calc_Pitch:")); Serial.println(d.pitch, 2);
            break;
        case MATH_4:
            Serial.print(F("Raw_gz_Speed:")); Serial.print(d.gz, 4);
            Serial.print(F(",Accumulated_Yaw:")); Serial.println(d.yaw, 2);
            break;
    }

    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    delay(50); // 20 Гц — комфортно для графиков в Grafana/Serial Plotter
}
```

---

## Заключение

Ученик выступает не пассивным слушателем, а исследователем: сам ставит эксперимент, снимает график, сверяет с формулой. Учитель освобождается от рутинного сбора данных и может сосредоточиться на физическом и математическом смысле происходящего.
