# Энергонезависимая память, тревоги и журнал

Источник: `C:\dump\fw_128k.bin`, ARM Thumb little-endian, адрес загрузки 0. Адреса ниже относятся к этому бинарнику. Сопоставлены инструкции из `C:\dump\Untitled-1.txt` и новая декомпиляция `C:\Temp\gas-re-tools\decompiled\functions`. Имена функций `FUN_...` — автоматически присвоенные адресные имена, а смысловые названия ниже — результат анализа.

## Что важно для новой прошивки

1. Код обращается к отдельной последовательной энергонезависимой памяти. В Flash микроконтроллера есть алгоритмы и исходные значения, но текущие нули, коэффициенты, настройки и журнал нужно читать отдельно с этой памяти.
2. Для обычных каналов есть две ступени превышения: предварительная и высокая. Для канала 1 при типе газа 1 работает другая логика — недостаток либо избыток. Просто один верхний порог для всех не повторит поведение прибора.
3. Журнал создаётся при входе в ступень тревоги и содержит канал, ступень, значение и дату/время. Повторяющаяся выборка в той же ступени новую запись не создаёт.
4. Восстановлены отдельные текущие и заводские значения калибровки; восстановление заводских данных не равно простому обнулению коэффициентов.
5. Унаследовать стоит функции; форматы хранения и некоторые пограничные решения требуют улучшения. В исследованных ветках нет самостоятельного гистерезиса, проверок CRC и атомарного подтверждения записи.

## Внешняя память: подтверждено инструкциями

| Адрес | Восстановленная операция | Доказательство |
|---|---|---|
| `0xEC50` | Установить номер банка 0 | Аргумент игнорируется; `0xEC50–0xEC54` записывают ноль в `0x20000058` |
| `0xEC5C` | Чтение одного байта | START, управляющий байт `0xA0`, два адресных байта, повторный START, `0xA1`, приём и STOP |
| `0xECF0` | Чтение блока | Сохраняет байты в буфер; приём разбит на порции по 64; адрес маскируется `0x7FFF` |
| `0xEECC` | Запись одного байта | START, `0xA0`, старший/младший адрес, байт данных; проверки ответа после передачи |
| `0xEF74` | Запись блока с учётом границы страницы | `64 - (address & 63)` для первой порции, затем по 64 байта; повторный START/STOP между страницами |
| `0x7FA0`, `0x7FE8` | START и STOP | Изменение линии данных при высокой линии тактирования |
| `0x8020`, `0x7F30` | Передать и принять байт | Побитовый цикл из восьми тактов и отдельный ACK/NACK |
| `0xF110` | Инициализация линий памяти | GPIOA, маски `0x800`, `0x1000`, `0x200` |

Это интерфейс I²C с 7-битным адресом `0x50`. Программно используются 15 адресных бит, то есть 32 КиБ, и страницы по 64 байта. Такая организация совместима с 24C256; точную маркировку микросхемы по коду установить нельзя.

Из локального SDK `fm33lg0xx.h` (строки 1006–1008): `GPIOA=0x40000C00`, `GPIOB=0x40000C40`, `GPIOC=0x40000C80`. Поэтому линии памяти — **PA11=SCL, PA12=SDA**. Например, `0x7F4E–0x7F7C` переключают PA11 и читают PA12. **PA9** опускается перед записью и поднимается после; роль WP весьма вероятна, но требует проверки дорожкой до микросхемы.

Внешняя память никак не является второй половиной `fw_128k.bin` и не может быть получена чтением адреса Flash `0x10000`.

### Восстановленные области памяти

| Адрес внешней памяти | Размер | Назначение и доказательство |
|---|---:|---|
| `0x0000 + 0x39*i`, i=0..3 | 57 на канал, всего 228 | Калибровочная структура в RAM `0x200014E8 + 57*i`; чтение `0x11152–0x11158`, запись `0x1EA2–0x1EB4` |
| `0x00E9` | 1 | Маркер `0x5A`, разрешающий восстановление сохранённых заводских значений; проверка `0x1115C–0x1116A`, запись `0x1141A–0x11420` |
| `0x0201 + 0x13*i` | 19 на канал | Настройки канала в RAM `0x20000EA3 + 19*i`; чтение `0x64AC–0x64C0`, запись `0x6688–0x669E` |
| `0x024D + 6*i` | 6 на канал | Ещё один каналозависимый блок, RAM `0x20000E8B + 6*i`; `0x69F4–0x6A22`. Поля ещё не названы |
| `0x1200` | 1 | Маркер журнала `0x5A`; проверка в `0x21B8–0x21C8`, запись при очистке `0x1F3C–0x1F42` |
| `0x1201` | 2 | Число записей little-endian; `0x13B8–0x13CC` |
| `0x1203 + 14*n` | 14 на запись | Записи тревог, n=0..1999; `0x13CE–0x13E4` |

Последняя допустимая запись занимает `0x7F55–0x7F62`; следующий свободный адрес `0x7F63`, всё помещается в 32 КиБ.

### Текущие и заводские поля

`0x112E8` читает калибровки и настройки, переносит текущие поля в запасные и сохраняет общий блок, затем ставит маркер `0x5A` по `0xE9`. `0x11150` выполняет обратный перенос при наличии этого маркера. Внутри 57-байтной калибровочной структуры:

| Текущее поле | Сохранённое заводское поле | Размер |
|---|---|---:|
| `+0x06` | `+0x10` | 2 |
| `+0x16` | `+0x20` | 2 |
| `+0x14` | `+0x1E` | 2 |
| `+0x31` | `+0x35` | 4 |
| `+0x28` | `+0x2A` | 2 |
| Настройки канала `+0x0E` (H) | Калибровки `+0x24` | 2 |
| Настройки канала `+0x10` (L) | Калибровки `+0x26` | 2 |

При восстановлении код также очищает первые пять элементов двух массивов 16-битных точек (`+0x04` и `+0x14`), затем заполняет опорные элементы из сохранённых полей. Это существенно для проектирования своей операции «заводские настройки»: она меняет и таблицу калибровки, и пороги.

## Тревоги

Основной фрагмент `0xC1F6–0xC6D0` читает:

- текущий канал `i`: byte `0x200000F8`;
- текущее значение `x`: **signed 16-bit** `0x20002044 + 12*i`;
- H: unsigned 16-bit `0x20000EA3 + 19*i + 0x0E`;
- L: unsigned 16-bit `0x20000EA3 + 19*i + 0x10`;
- предыдущее состояние: byte `0x200000CA + i`, значения 0/1/2.

| Режим | Норма, состояние 0 | Ступень L, состояние 1 | Ступень H, состояние 2 |
|---|---|---|---|
| Каналы 0, 2, 3; канал 1 при типе !=1 | `x < L` | `L <= x < H` | `x >= H` |
| Канал 1 при типе 1 | `L <= x < H` | `x < L` | `x >= H` |

Доказательства: сравнения `0xC230–0xC232`, `0xC2A8–0xC2CE`, `0xC34A–0xC34C`; специальная ветка `0xC376–0xC39E`, `0xC466–0xC490`, `0xC4D8–0xC4DA`. Границы различаются: при равенстве L обычный канал уже тревожится, кислородный ещё в норме; равенство H вызывает высокую тревогу.

В специальной ветке есть дополнительная проверка первого 16-битного поля калибровки: если оно ноль, эта ветка тревог вообще пропускается (`0xC39C–0xC39E`). Предыдущее состояние при этом не обязательно очищается. Смысл самого поля уточняется в анализе измерений; поведение нельзя переносить без проверки.

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

При входе в H записывается состояние 2 и событие `0x10`; при входе в L — состояние 1 и событие `0x01`. При неизменной ступени записи не повторяются. Переход H→L создаёт событие L, если показание осталось в диапазоне L; возвращение в норму в этом фрагменте не записывается. После обработки индекс канала увеличивается по кругу 0..3.

### Значения из инициализированной RAM

Из `evidence/startup_ram.bin`, полученной основным исследованием распаковки стартовых данных:

| Канал | Код газа | Десятичных знаков (`config+3`) | H | L |
|---|---:|---:|---:|---:|
| 0 | 0 | 0 | 50 | 20 |
| 1 | 1 | 1 | 235 | 195 |
| 2 | 2 | 0 | 20 | 10 |
| 3 | 3 | 0 | 200 | 50 |

При масштабе одного десятичного знака значения канала 1 — 23.5 и 19.5. Эти числа — встроенные исходные значения. Они не доказывают, какие пороги сейчас находятся в конкретном приборе: загрузка из внешней памяти их заменяет.

### Выходные сигналы и приоритет

`0x1534` обрабатывает все четыре состояния. Наличие любого состояния 2 выбирается раньше состояния 1. Есть разные последовательности тактов: высокие тревоги используют параметры 3/4/4 и повтор через 14 вызовов; низкие — 3/4/6 и повтор через 12 вызовов (`0x15A4–0x15E6`, `0x1702`, `0x175A`). Переводить эти вызовы в миллисекунды можно только после проверки вызывающего планировщика.

Функция управляет PC4, PA6, PB5 и PC1. Точное соответствие «звук / вибрация / свет» требует трассировки платы. Byte `0x200000ED` подавляет включение PC4 в нескольких ветках (`0x1600–0x160E`, `0x167E–0x168C`, `0x16BE–0x16CC`), что соответствует функции отключения одного вида оповещения. Bytes `0x200000ED` и `0x200000EE` сбрасываются, когда все четыре состояния нормальны (`0xC694–0xC6BA`).

## Формат и жизненный цикл журнала

`0x1360(channel, type)` формирует 14 байт:

| Смещение | Поле | Уверенность |
|---:|---|---|
| 0 | Индекс канала | Подтверждено аргументом и чтением в просмотрщике |
| 1 | Неинициализированный/резервный байт | В просмотренном писателе запись отсутствует |
| 2 | Маркер `0x5A` | Подтверждено |
| 3 | `0x10`=H, `0x01`=L | Подтверждено ветками тревог и просмотрщиком |
| 4–5 | Последнее значение H, little-endian | При H обновляется перед вызовом писателя |
| 6–7 | Последнее значение L, little-endian | При L обновляется перед вызовом писателя |
| 8–10 | Три компонента даты | Источник RAM `0x20000E8A`, `0x20000E88`, `0x20000E89` |
| 11–13 | Три компонента времени | Источник RAM `0x20000E85..0x20000E87` |

При событии H валидным актуальным значением является поле 4–5; при L — 6–7. Второе поле копируется из общей структуры и может содержать старое значение, в том числе от другого канала. Просмотрщик `0x2188` выбирает нужное поле по байту типа (`0x2250–0x22FA`).

Сначала пишется запись, затем число записей. Результаты этих операций в `0x1360` не проверяются. При достижении 2000 код **записывает счётчик 0** и переходит в состояние интерфейса 150 (`0x13F2–0x1426`). Поэтому это не доказанный полноценный кольцевой журнал с сохранённым числом заполненных записей. При следующей записи адрес опять начнётся с `0x1203`; просмотрщик при числе 0 считает журнал пустым (`0x21CA–0x21D0`). Это важный кандидат на улучшение.

Очистка журнала (`0x1F18`, `0x1FA8`) ставит маркер `0x5A` и обнуляет число по `0x1201`; все старые записи физически не стирает.

Текущий просмотр проверяет маркеры и допустимость отдельных полей; CRC целой записи в рассмотренных функциях не найден. Возможное наличие отдельного журнала ошибок по одной строке меню не подтверждается: найден и связан с реальной логикой именно журнал газовых тревог.

## Что улучшить в собственной реализации

- Ввести явный тип пороговой схемы: две верхние ступени или допустимый интервал. Не привязывать смысл к номеру физического канала.
- Разделить состояния газа, неисправности датчика, низкого заряда и оповещателей; задать приоритеты одновременных событий.
- Добавить настраиваемый гистерезис и проверяемую политику подавления повторов. Проверить значения ровно на L/H и дрожание около них.
- Для отключения звука задать явные правила: что продолжает работать, когда звук включается снова, что происходит при новой высокой тревоге.
- Хранить настройки и калибровки с версией, длиной, CRC и двумя копиями/номером поколения. Сохранять заводскую копию отдельно от текущей.
- Проверять статус внешней памяти, время ожидания и фактическое чтение после записи. На ошибке восстанавливать состояние линии защиты записи: исследованные ранние возвраты из `0xEECC`/`0xEF74` обходят финальное поднятие PA9.
- Журналу нужны самостоятельные head/count/sequence, определённая политика переполнения и восстановление после обрыва питания. Каждый байт записи должен быть инициализирован. Сохранять одно актуальное значение плюс явные флаги валидности.
- Записывать не только вход, но и снятие тревоги, отказ датчика, изменение калибровки/порогов и подтверждение оператором, если это входит в требования изделия.

## Проверка

`verify_alarm.py` исполняет исходный фрагмент `0xC1F6–0xC6D0` в ARM-эмуляторе с контролируемой RAM; только вызов внешнего писателя `0x1360` заменён сбором аргументов. **Все 108 проверок прошли** на Linux, Python 3.13, Unicorn 2.1.4: 90 комбинаций границ/предыдущих состояний, сохранение отключения при тревоге другого канала, 9 вариантов нулевого поля калибровки и 8 проверок исходного писателя журнала `0x1360–0x1434`.

В восьми проверках журнала исполняется настоящий писатель, а на границах внешних подсистем подставляются EEPROM, обновление RTC и вывод интерфейса. Для счётчиков 0, 1998, 1999 и 2000 проверены успешная и неуспешная запись памяти. Подтверждены адреса записей, обнуление счётчика после 2000-й, продолжение обновления счётчика даже при возвращённой ошибке записи и попадание неинициализированного байта стека в поле +1. Эти результаты сохранены в `emulation_results.json`.

Windows-сборки Unicorn 2.1.4/2.0.1 на данном компьютере завершались с ошибкой во время выделения памяти/старта эмуляции; эти попытки не учитываются как проверки. Успешный независимый запуск выполнен в WSL.

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