# Как устроен датчик и как мы разгадали его протокол

## Мультиметр и осциллограф

Мультиметр показывает одно число: напряжение «сейчас», усреднённое за
доли секунды. Если напряжение меняется быстрее, чем обновляется экран,
мультиметр покажет что-то среднее, и вы не узнаете, что было на самом деле.

Осциллограф показывает **график**: по горизонтали время, по вертикали
напряжение. На нём видно, как батарейка проседает в момент включения мотора,
как кнопка «дребезжит» при нажатии и почему ШИМ-светодиод только кажется
горящим вполсилы.

## Шаг 1. Напряжение → число: АЦП

Внутри датчика стоит **АЦП**, аналого-цифровой преобразователь. Он меряет
напряжение и выдаёт целое число от 0 до 4095. Это 12 бит: 2¹² = 4096 ступенек.

Датчик меряет и плюс, и минус, поэтому ноль вольт находится посередине шкалы,
около кода 2048. Всё, что выше, — плюс, что ниже — минус:

```
код  4095  ─── +предел
код  2048  ───   0 В
код     0  ─── −предел
```

Перевод кода в вольты — одна строка в [`app/index.html:271`](app/index.html):

```js
(code - zero) * mv_per_code / 1000
```

**Проверим на числах.** Предел ±5 В, на один код приходится 2,48 мВ. Пришёл код
3370, ноль у этого канала — 2040:
(3370 − 2040) · 2,48 мВ = 3298 мВ ≈ 3,3 В. Это верхний уровень меандра ESP32.

## Шаг 2. Предел: чем меньше, тем точнее

Ступенек всегда 4096, а предел можно выбрать. Значит, меняется цена одной
ступеньки:

| Предел | Одна ступенька | Батарейку 1,5 В видно как… |
|---|---|---|
| ±90 В | 44 мВ | 34 ступеньки: просадку в 20 мВ не заметить |
| ±5 В | 2,5 мВ | 600 ступенек |
| ±2 В | 1 мВ | 1500 ступенек: видна просадка в пару милливольт |

Правило такое же, как с мультиметром: **самый маленький предел, при котором
нет зашкала**. Числа взяты из калибровки ([WIRING.md](WIRING.md#калибровка)) и
записаны в [`app/b202.json`](app/b202.json), раздел `calibration`.

## Шаг 3. Как часто мерить: частота оцифровки

АЦП меряет не непрерывно, а через равные промежутки времени. Например, «точка
через 100 мкс» — это 10 000 замеров в секунду (10 кГц).

Если мерить слишком редко, сигнал потеряется. Меандр 100 Гц при оцифровке
200 Гц даст две точки на период: не поймёшь, меандр это, синусоида или
пила. Для хорошей картинки нужно **хотя бы 10–20 точек на период**.

Если точек меньше двух на период, прибор не просто теряет сигнал, а
показывает другой. Проверено со звуковой картой: при точке через 100 мкс тон
9900 Гц на экране — ровная волна 98 Гц, тон 6000 Гц — 4 кГц. Урок об этом —
[«Прибор врёт»](Lesson.md#урок-прибор-врёт-8-класс-45-мин).

Датчик записывает **кадр** из 3072 точек и только потом передаёт его
целиком. Поэтому частота оцифровки определяет и длину кадра:

| Точка через | Частота | Длина кадра | Для чего |
|---|---|---|---|
| 10 мкс | 100 кГц | 31 мс | дребезг кнопки, ШИМ |
| 100 мкс | 10 кГц | 0,3 с | 50 Гц от трансформатора |
| 1 мс | 1 кГц | 3 с | батарейка, конденсатор |
| 5 мс | 200 Гц | 15 с | медленные процессы |

## Шаг 4. Синхронизация: почему картинка стоит на месте

Каждый кадр начинается в случайный момент. У повторяющегося сигнала
картинка от кадра к кадру сдвигается и «бежит».

Синхронизация (на настоящих осциллографах — «запуск», trigger) велит датчику
ждать, пока сигнал выбранного канала пересечёт заданный уровень в нужную
сторону, и только тогда начинать кадр. Каждый кадр начинается в одной и той же
фазе сигнала, и картинка стоит.

Уровень датчику передаётся кодом АЦП, а не вольтами. Пересчёт вольт в код —
обратная формула из шага 1, она в [`app/index.html:272`](app/index.html).

Для одиночных событий (нажатие кнопки, заряд конденсатора) есть режим «Один
кадр»: датчик ждёт события, записывает кадр и останавливается.

## Как мы разгадали протокол

Программа производителя не рассказывает, как говорит с датчиком. Разгадывали
по шагам, и каждый шаг можно повторить на уроке информатики.

**1. Что видит Linux.** Команда `lsusb` показала устройство
`0403:6001 FTDI FT232R`. Это стандартный мост USB ↔ UART (последовательный
порт). Драйвер для него встроен в Linux, и датчик появился как `/dev/ttyUSB0`.

**2. Датчик молчит.** Сам по себе он ничего не передаёт: ждёт команды. Перебирать
команды наугад бесполезно.

**3. Подсказка из Android.** У производителя есть приложение для Android,
написанное на Kotlin. Такие программы можно разобрать обратно в почти
читаемый код (декомпилировать). В нём нашлись текстовые команды:

| Команда | Что делает |
|---|---|
| `I` | «кто ты?» → `B202,3.02,1.12.2011,2,…,921600` |
| `C 6 FREQ=?` | спросить параметр; число 6 — длина `FREQ=?` |
| `C 8 FREQ=100` | установить параметр |
| `S` / `T` | начать / остановить измерение |

Команды идут на скорости 38400 бод, данные — на 921600
([`app/b202.json`](app/b202.json), `baud`).

**4. Данные полубайтами.** Каждый байт данных делится на две половинки по
4 бита, и каждая означает символ:

| Полубайт | Значение |
|---|---|
| `0`–`9` | цифра кода АЦП |
| `C` | конец числа |
| `D` + номер | начало кадра канала |
| `F` | заполнитель |

Байты `20 60 CC` читаются как «2060, конец». Разбор потока — класс
класс `FrameParser` в [`app/xstem-scope.js`](app/xstem-scope.js); тот же разбор на Python —
`FrameParser` в [`app/xstem-scope.py`](app/xstem-scope.py). Что оба разбирают поток одинаково,
проверяют тесты в [`tests/`](tests/test_protocol.py).

**5. Ловушки, на которые мы наступили.**

- **«T» надо слать на скорости данных.** Посланная на 38400 команда «стоп»
  до датчика не доходила, и он продолжал присылать кадры, не слушая
  новых команд (`_stop()` в [`app/xstem-scope.js`](app/xstem-scope.js)).
- **`FREQ` — не частота, а период в микросекундах.** На `FREQ=10000` мы ждали
  10 кГц, а датчик честно записывал 30 секунд с частотой 100 Гц. Разгадали
  по меандру ESP32: 100 Гц при `FREQ=1000` дали ровно 10 точек на период.
- **Недопустимые значения датчик молча заменяет своими.** Подходят только
  10, 20, 50, 100, 200, 500… ([`app/b202.json`](app/b202.json), `periods_us`).
- **Первые секунды после включения датчик мерит по-своему.** Команды он
  подтверждает, и прочитанные обратно настройки — наши, но «Пуск» в первые
  3–4 секунды идёт с его собственными: точка через 10 мкс и синхронизация по
  К1. На пустом входе кадров тогда нет вовсе, а с шумом они сыплются втрое чаще
  положенного. Записали, выдёргивая датчик посреди опыта: «Пуск» через 1,5–2,5 с —
  его настройки, через 4,3 и 13 с — наши. Поэтому программа, найдя вставленный
  датчик, ждёт 4 секунды ([`app/b202.json`](app/b202.json), `boot_s`).
- **Датчик копит кадр целиком.** Проверили по времени прихода байтов: на
  кадре 6 с первые данные приходят через 6,3 с, и весь кадр — за 0,3 с. Зато
  `PACKET_SIZE` можно не только спросить, но и задать: при 250 точках кадры
  идут каждые 0,53 с. На этом построен самописец. Параметр датчик помнит, пока
  включён, поэтому программа каждый раз ставит его явно, а при подключении
  возвращает 3072.

**6. Калибровка по известному сигналу.** Пока на входе ничего нет, код
ничего не говорит о вольтах. Нужен сигнал, который мы знаем заранее: меандр
0/3,3 В от ESP32 ([`src/main.cpp`](src/main.cpp)). Размах в кодах на каждом
пределе дал «милливольты на код».

Общий принцип: **неизвестный прибор изучают известным сигналом**. Так же
проверяют и настоящие осциллографы: у них на передней панели есть
вывод «калибратор» с меандром 1 кГц.
