# Теория: как GY-271 находит север

## 1. Магниторезистивный эффект: как чип «чувствует» магнитное поле

Оба чипа, которые встречаются под платой "GY-271" (HMC5883L и QMC5883L),
используют один и тот же физический принцип — **анизотропный
магниторезистивный эффект (AMR)**. Внутри чипа есть тонкие полоски пермаллоя
(сплав никеля и железа), электрическое сопротивление которых меняется в
зависимости от направления и силы внешнего магнитного поля. Три таких
элемента расположены перпендикулярно друг другу (по осям X, Y, Z), поэтому
чип может измерить не просто «есть поле или нет», а вектор магнитного поля
в трёхмерном пространстве.

Магнитное поле Земли — как раз то поле, которое эти элементы улавливают.
Зная его направление, можно вычислить, куда указывает «магнитный север».

## 2. Почему одна и та же плата бывает разной внутри

"GY-271" — это название платы-переходника, а не название чипа. Производители
плат меняли начинку: старые платы несут оригинальный **Honeywell HMC5883L**,
более новые — **QST QMC5883L**. Это разные производители, разные I2C-адреса
и разные карты регистров — код, написанный под один чип, не будет работать
с другим, если явно не учесть эту разницу.

`begin()` в этом драйвере ([Gy271Compass.cpp:5-20](src/Gy271Compass.cpp#L5-L20))
устроен как последовательная проверка: сначала пробуем адрес `0x0D`
(QMC5883L), и если чип по этому адресу подтверждает свой ID — работаем с ним.
Если нет — пробуем `0x1E` (HMC5883L). Так один и тот же код работает с любой
платой, которая реально попадёт в руки, без необходимости знать заранее,
какая именно версия куплена.

## 3. Регистры, которые мы используем

Источники: датащит QST QMC5883L (Rev. 1.0/B, раздел "Register Map") и
датащит Honeywell "3-Axis Digital Compass IC HMC5883L" (регистровая карта).

### QMC5883L (адрес `0x0D`)

| Регистр | Адрес | Что делает | Где в коде |
|---------|-------|------------|------------|
| Data Output | `0x00-0x05` | X,Y,Z по 2 байта каждая (LSB, затем MSB), порядок в памяти уже X,Y,Z | [Gy271Compass.cpp:59-68](src/Gy271Compass.cpp#L59-L68) |
| Status | `0x06` | Бит DRDY — данные готовы | [Gy271Compass.h:56](include/Gy271Compass.h#L56) |
| Control 1 | `0x09` | Режим измерения, частота обновления, диапазон, передискретизация | [Gy271Compass.cpp:44](src/Gy271Compass.cpp#L44) |
| SET/RESET Period | `0x0B` | Служебный регистр, датащит рекомендует записать `0x01` | [Gy271Compass.cpp:43](src/Gy271Compass.cpp#L43) |
| Chip ID | `0x0D` | Фиксированное значение `0xFF` — по нему драйвер узнаёт чип | [Gy271Compass.cpp:40-41](src/Gy271Compass.cpp#L40-L41) |

### HMC5883L (адрес `0x1E`)

| Регистр | Адрес | Что делает | Где в коде |
|---------|-------|------------|------------|
| Configuration A | `0x00` | Усреднение по выборкам, частота обновления | [Gy271Compass.cpp:53](src/Gy271Compass.cpp#L53) |
| Configuration B | `0x01` | Усиление приёмника (gain) | [Gy271Compass.cpp:54](src/Gy271Compass.cpp#L54) |
| Mode | `0x02` | Режим измерения (непрерывный/разовый) | [Gy271Compass.cpp:55](src/Gy271Compass.cpp#L55) |
| Data Output | `0x03-0x08` | **Важная особенность**: физический порядок в памяти — X (0x03-0x04), затем Z (0x05-0x06), затем Y (0x07-0x08). Не X,Y,Z! | [Gy271Compass.cpp:70-79](src/Gy271Compass.cpp#L70-L79) |
| Identification A | `0x0A` | Возвращает ASCII `'H'` (0x48) — по этому байту драйвер узнаёт чип | [Gy271Compass.cpp:49-51](src/Gy271Compass.cpp#L49-L51) |

Перестановка Y и Z для HMC5883L — не ошибка драйвера, а особенность датащита
Honeywell: если читать байты подряд как X,Y,Z, компас будет «врать» по оси Z.
Драйвер делает эту перестановку один раз внутри `readRawHmc5883()`, поэтому
наружу (`readRaw()`) обе версии чипа отдают показания в одинаковом порядке
X, Y, Z — вызывающему коду не нужно знать, какой чип реально на плате.

## 4. Калибровка Hard-Iron: без неё никак

Идеальный магнитометр в идеальном мире при повороте на 360° нарисует на
графике X-Y идеальную окружность с центром в точке (0,0). В реальности рядом
с чипом почти всегда есть металл — дорожки платы, корпус ESP32, крепёж — и
эти предметы либо намагничены сами (**Hard-Iron** искажение — сдвигает центр
окружности от нуля), либо искажают проходящее через них магнитное поле
(**Soft-Iron** искажение — превращает окружность в эллипс).

Простой и понятный на школьном уровне способ скомпенсировать Hard-Iron —
найти реальные границы (минимум и максимум) показаний по X и Y при полном
повороте платы, и потом «сжать» эти границы обратно в диапазон -1..1:

```
x_calibrated = (x_raw - x_min) / (x_max - x_min) * 2 - 1
```

Именно так это реализовано в тестовом стенде: `startCalibration()` и
`updateCalibration()` ([main.cpp:66-100](src/main.cpp#L66-L100)) в течение
30 секунд ищут `xMin/xMax/yMin/yMax`, а `computeHeadingDeg()`
([main.cpp:39-52](src/main.cpp#L39-L52)) применяет эту формулу перед расчётом
угла. Soft-Iron (эллипс) эта простая калибровка не устраняет полностью, но
для школьного проекта точности в несколько градусов обычно достаточно.

Калибровочные границы **не хранятся в драйвере** — они зависят от конкретной
платы, её крепления и того, что рядом с ней в момент калибровки. Драйвер
отдаёт только сырые показания; калибровка — задача тестового стенда,
которую нужно повторить, если сильно изменилось окружение датчика (например,
плату перенесли на металлический корпус робота).

## 5. От вектора поля к азимуту

Зная скомпенсированные X и Y, направление на магнитный север — это угол
вектора (X, Y) на плоскости, который вычисляется через `atan2`:

```
heading = atan2(y_calibrated, x_calibrated)
```

`atan2` (в отличие от обычного `atan`) сразу учитывает знаки обеих
координат и возвращает угол во всех четырёх четвертях, поэтому не нужно
вручную разбирать случаи "X отрицательный" и т.п. — [main.cpp:45](src/main.cpp#L45).

## 6. Магнитное склонение: магнитный север ≠ географический

Магнитный полюс Земли не совпадает с географическим полюсом, поэтому
магнитометр указывает не совсем туда, куда показывает компас на карте.
Разница между ними — **магнитное склонение** — зависит от того, где вы
находитесь, и медленно меняется год от года. Готового универсального
значения нет: `MAGNETIC_DECLINATION_DEG` в [main.cpp:10](src/main.cpp#L10)
нужно подставить под свой регион (например, по данным
magnetic-declination.com) — компонент намеренно не содержит "зашитого"
значения для одного конкретного города.
