Измерительный тракт FM33LG025: анализ и проверки машинного кода
Источник: C:\dump\fw_128k.bin, SHA-256
fa0ff6bb9fdd148ac1449d6a12b491c04434ec47ca6283439f0b6e6875a62560.
Старый экспорт C:\dump\Untitled-1.txt использован как навигация. Скрипт
verify_export.py проверил 11 302 байта в рассматриваемых диапазонах против
бинарника: несовпадений нет. Утверждения ниже основаны на инструкциях и данных,
а не на наличии строк меню. Все адреса функций — адреса Flash, без Thumb-бита.
Проверка периферии: локальный комплект FMSH
E:\FM33LG0XXbooload软件V4.4(22.07.22) содержит fm33lg0xx.h и
fm33lg0xx_fl_adc.h. В них ADC IRQ = 5, ADC base = 0x40015C00,
DMA base = 0x40000400; ADC CHER offset = 0x20, DR = 0x28.
Официальный канал получения альтернативного пакета:
FMSH FM33LG0XX_DFP в Arm Keil.
Загрузка этого пакета для анализа не потребовалась.
Что установлено
В бинарнике есть самостоятельный измерительный тракт с четырьмя газовыми
слотами, каналом температуры и каналом батареи. Газовые значения проходят
несколько последовательных фильтров, выбираемую по типу датчика калибровку,
условную температурную коррекцию, поправку нуля и дополнительные преобразования
показаний. Это существенно сложнее одного усреднения и линейного коэффициента.
Дополнительно выполнены 102 проверки оригинального ARM-кода в Unicorn
в Linux/WSL, все прошли. Проверены фильтры, четыре калибровочных узла,
обе полярности, экстраполяция, нелинейная модель, три температурных модели,
настоящий обработчик ADC и условные зоны фиксации показаний. Результаты:
emulation_results.json, воспроизводящий сценарий emulate_measurement.py.
Периферийные адреса в этих проверках — только память эмулятора;
аппаратный restart ADC заглушён, а выбор каналов и фильтрация выполнены
оригинальными инструкциями. Зоны фиксации проверены как изолированные
участки функции обработки, до вызовов тревог.
ADC и порядок каналов
0x11C0 настраивает ADC: 12 бит, прямой обход каналов, аппаратное усреднение
8 преобразований со сдвигом 3, DMA и прерывание конца последовательности.
Константы передаются через структуру драйвера (0x1248…0x1256); поля и маски
сверены с локальным SDK. 0x127C…0x128A выбирает VREFP; далее включается
буфер, задаётся делитель APB и разрешается DMA.
0xC6F8 включает каналы 6, 13, 14, 15, 5, 27; порядок вызовов включения не
задаёт порядок сканирования. Устанавливается DMA на 6 halfword в
0x20000F10, после чего ADC включается и запускается программно.
Канал 27 по SDK — внутренний VBAT_DIV3, а не газовый датчик.
Вектор Flash 0x54 содержит 0x00000F5D, то есть обработчик ADC 0xF5C.
Он сохраняет шесть принятых результатов в шесть массивов с шагом 0x40 байт
от 0x20000F60. После десяти последовательностей по каждому каналу считает:
mean = (sum(samples[0..9]) - max(samples) - min(samples) + 4) >> 3
scaled = (mean * 2500) >> 12
Последняя операция реализована с 32-битным сдвигом влево на 4 и вправо на 16;
для нормального 12-битного ADC она эквивалентна указанной формуле. Масштаб
2500 согласуется с представлением милливольт при опоре 2,5 В; фактическую
точность опоры из дампа определить нельзя.
| Индекс сканирования | Вход ADC | Куда записан scaled | Использование |
| 0 | CH5 | 0x200011FA | Температура |
| 1 | CH6 | 0x200011F0 | Газовый слот 0: EX |
| 2 | CH13 | 0x200011F2 | Газовый слот 1: O2 |
| 3 | CH14 | 0x200011F6 | Газовый слот 3: CO |
| 4 | CH15 | 0x200011F4 | Газовый слот 2: H2S |
| 5 | CH27 / VBAT÷3 | 0x200011F8 | Батарея |
Этот порядок восстановлен из реальной таблицы переходов после вызова
0xF1D2 в 0x109C: её данные 06 04 0E 19 23 2D 37 41 по адресу 0x10A0
задают ветви 0x10A8, 0x10BC, 0x10D2, 0x10E6, 0x10FA, 0x110E.
Старый экспорт и первоначальная автоматическая декомпиляция ошибочно читают
эту таблицу как инструкции. Их псевдокод обработчика в этом месте непригоден.
Последующие фильтры и RAM
Основная функция измерений 0xAEC4 вызывается из обработчика основного цикла
0x1056C (вызов 0x1059E). Текущий газовый слот хранится в 0x200000F8;
за вызов обрабатывается один слот, в конце индекс увеличивается и после 3
сбрасывается на 0. Дополнительный анализ планировщика дал номинально 100 мс
на слот и 400 мс на повтор одного из четырёх включённых каналов;
условия и отличие от физического времени описаны в timing.md.
| RAM | Роль, подтверждённая обращениями |
0x20000014 | Индекс 0…9 накопления ADC |
0x20000F10 | Шесть halfword результатов DMA |
0x20000F60 + 0x40*j | Буфер из десяти 32-битных отсчётов скан-канала j |
0x20001210 + 4*j | Сумма с удалёнными минимумом/максимумом и округлением |
0x20001230 + 4*j | Усечённое среднее ADC |
0x200011F0 + 2*ch | Масштабированный вход газового слота |
0x20000020 + ch | Флаг инициализации скользящего среднего |
0x20000028 + 4*ch | Текущая сумма скользящего среднего |
0x20001250 + 0x20*ch | Восемь 32-битных отсчётов скользящего среднего |
0x200012F0 + 4*ch | Указатель кольцевого буфера |
0x200011E0 + 2*ch | Сглаженный вход калибровки |
0x20001200 + 4*ch | Предыдущий сглаженный вход |
0x200014E8 + 0x39*ch | 57-байтная запись калибровки в RAM |
0x20000EA3 + 0x13*ch | 19-байтная запись типа, пределов и настроек слота |
0x20002044 + 0x0C*ch | Итоговое значение, signed 16-bit, используемое далее |
0xFE48(ch, x) реализует скользящее среднее восьми значений. При первом
вызове все восемь позиций заполняются первым отсчётом, чтобы не начинать с
нуля. Затем вычитается самый старый отсчёт и прибавляется новый; указатель
идёт назад с заворотом, результат — целочисленное деление суммы на 8.
0x1044C(new, old) получает оба значения как float и возвращает округлённое
0.8*new + 0.2*old. Константы 0x3F4CCCCD, 0x3E4CCCCD и double 0.5
проверены; результат сохраняется для следующего вызова. Таким образом,
последовательность фильтров — аппаратные 8×, усечённое среднее 10→8,
скользящее среднее 8, затем рекурсивное сглаживание.
Калибровка
0x9A7C — возрастающая кусочно-линейная характеристика. Сигнальные узлы
x0…x3 читаются как little-endian u16 из записи +0x14,+0x16,+0x18,+0x1A,
соответствующие y0…y3 — из +4,+6,+8,+0xA. Последний ненулевой x выбирает
максимальный индекс 0…3. Между узлами выполняется линейная интерполяция;
ниже первого — 0, выше последнего — экстраполяция последнего сегмента.
Нормализация /100 и /10000 сокращается в формуле; конечный результат
округляется и отрицательный ограничивается нулём. Путь без заполненных узлов
имеет собственные константы по умолчанию, а не ошибку «не калиброван».
0xA9A4 — вариант с убывающим сигналом при росте показаний, использует те же
поля и обратные сравнения. Его выбирает флаг настройки слота +0x12 == 1.
0x9DD8 и 0xA134 — дополнительные варианты линейного пересчёта/масштабов
для слота 0. Формат записи позволяет четыре узла: это не доказательство
поддержки пяти независимых калибровочных точек старым прибором.
0xA490 — нелинейная ветвь слота 1 с типом 1. По арифметическим инструкциям:
z = u16(record + 0x14)
k = float32(record + 0x31)
if abs(k) < 0.0002 or input <= z: return 0
otherwise approximately round(1000 * (1 - exp(-(input-z)/(1000*k))))
Экспонента вычисляется через pow(e, x) в 0x10754; в Flash лежит
e = 2.71828182845904. В реализации промежуточные операции float/double
и преобразования u16/i16, поэтому эта математическая запись не обещает
побитного совпадения на краевых значениях. Первоначальный декомпилятор теряет
флаги сравнения double и ошибочно делает ветвь недостижимой: проверять надо
инструкции 0xA4EC…0xA502.
При режиме 0xA5 и предыдущем показании ниже 276 дополнительно вызывается
0xA5A0: полиномиальная поправка с коэффициентами 0.745605608213618,
0.352996450822339, 0.056530584786830 и 0.523734370792199. Режим 0x5A
её обходит. Физическое происхождение такой поправки не установлено.
Температура и компенсация
0x11478 раз в 20 своих вызовов читает канал CH5, дополнительно усредняет
его функцией 0xFF20 и считает (millivolts - 495) / 10 с целочисленным
делением, затем сохраняет signed byte в 0x200000C4. Это соответствует
внешнему датчику с линейным наклоном 10 мВ/°C; его модель надо проверить по
плате. Встроенный ADC_TS (канал 25) в этой последовательности не включён.
Температура при калибровке хранится signed byte в записи +0x30. Компенсация
применяется только в диапазоне -17…62 и при abs(T-Tcal) >= 5.
Для типов 2 и 3 вызывается 0xA6AC(input, record, T, type). В этой ветви
используется двухточечная характеристика с x0 из +0x14, особым сохранённым
span из +0x28, y0/y1 из +4/+6; температурный коэффициент — полином:
y = y0 + (y1-y0) * (input-x0) / ((span-x0) * P_type(T))
P_type(T) = c0 + c1*T + c2*T² + c3*T³ + c4*T⁴
| Тип | Адрес коэффициентов | c0 | c1 | c2 | c3 | c4 |
| 2 | 0x114F4 | 0.944262 | 0.003837432 | -0.0000532984 | 0.0000001164 | 0 |
| 3 | 0x11508 | 0.77103716 | 0.0150139 | -0.00009775 | -0.00000582 | 0.00000009 |
Тип 1 получает компенсацию после начального пересчёта в 0xE758:
Q(T) = -0.00001845072*T² + 0.0027042576*T - 0.035001203
result = round(raw * (1 + Q(Tcal) - Q(T)))
В 0xAEC4 есть также локальные корректировки отдельных диапазонов:
например, для типа 3 при T<8 и 199<value<450 прибавляется 3; при T>34
и value>84 вычитается 6. Для типа 2 есть отдельные ветви диапазонов 14…17
и 70…100. Это эмпирическая логика конкретного прибора; её перенос без
измерений на собственных датчиках необоснован.
Дополнительные преобразования показаний
После пересчёта запись +0x2F задаёт знак ASCII + или -, а u16 +0x2C
— величину добавляемого/вычитаемого смещения. Результат ограничивается нулём.
Далее есть зона фиксации показаний: если `metadata[+0xA] < value <=
metadata[+8], значение заменяется metadata[+0xC]`. Для исходных defaults
слота 1 это 203 < value <= 216 → 209. Для остальных defaults нижняя
граница 0, верхняя 3/2/6, замена 0. Это не фильтр среднего, а явное изменение
передаваемого значения. При определённых режимах слота 0 дополнительные
ветви фиксируют 45…55 → 50 и 190…210 → 200; границы рядом преобразуются
плавными функциями. Условия: режим 0xA5, slot 0, metadata unit 8, subtype
11, не режимы 30/50, и неубывающий исходный результат.
В эмуляции подтверждено: 204, 209, 216 заменяются на 209; для EX 1…3,
H2S 1…2 и CO 1…6 заменяются нулём. Отдельный режим слота 0 проверен на
45/50/55 и 190/200/210 с результатами 50 и 200 соответственно. Это
подтверждает именно программное поведение на синтетических входах, а не
погрешность измерения реального газа.
Перед проверкой тревог итог ограничивается диапазоном [0, metadata.+4].
Проверки тревог используют тот же 0x20002044 + 12*ch уже после этих
преобразований. Следовательно, нельзя без проверки считать фиксацию показаний
чисто косметической обработкой экрана.
Батарея
0x1F04 считает battery_mV = 3*scaled_CH27 + 200.
0x1A8C запускает обработку раз в 10 вызовов, применяет дополнительное
среднее 0xFF20 и сохраняет результат в 0x200000C0. Ветви используют
3650, 3555 и 3455. При значении выше 3650 сбрасывается флаг 0x200000E8;
ниже 3555 при флаге 0 выбирается режим 7, вызываются 0xCF00(7,7,7)
и 0x1ED8. Ниже 3455 при флаге 1 устанавливается флаг 2, изменяются
GPIOC и вызывается 0xE82C, после чего выполняется бесконечный цикл.
Полное назначение флагов/зарядки и последовательность физического
выключения ещё не восстановлены. Прибавка 200 — программный offset, а не
универсальная характеристика ADC или аккумулятора.
Что это означает для нового ядра
- Отделить исходный ADC, фильтрованный сигнал, калиброванное значение,
значение с температурной компенсацией, значение для тревог и для экрана.
В диагностическом режиме сохранять каждый этап.
- Поддержать направление характеристики, несколько калибровочных узлов,
отдельный ноль, span и температуру калибровки. Проверять монотонность,
одинаковые x, конечность коэффициентов, выход за диапазон и отсутствие
калибровки явным статусом.
- Профиль датчика должен задавать фильтрацию и температурную модель;
частоту, задержку и время достижения тревоги измерять вместе. Повторять
все четыре слоя фильтра без оценки задержки не следует.
- Коэффициенты старого прибора привязаны к его аналоговой схеме, датчикам,
диапазонам и эмпирическим исправлениям. В новой прошивке нужны собственные
измерения; формулы из дампа полезны как референс и набор проверяемых идей.
- Явно определить, допускается ли округление/зона нуля только на экране;
тревоги и журнал должны получать выбранное инженерно обоснованное
значение. Любую замену диапазона значений одним числом документировать.
- Добавить независимые каналы температуры и батареи, проверку правдоподобия,
политики отказа датчика, отсутствия новых измерений и насыщения ADC.
Ограничения и воспроизводимость
Ни J-Link, ни аппаратная память в этой работе не открывались. RAM defaults
взяты из распакованного startup-блока, а не с текущего работающего прибора.
Реальные индивидуальные калибровки читаются из внешней NVM; её содержимое
нельзя восстановить из значений, лежащих в RAM только после старта.
Названия исходных слотов подтверждены отдельным анализом UI: 0x3B74
получает тип из metadata +2, 0x77E8 при language=1 выбирает строку
0x20000A8C + type*11; в распакованных данных type0=EX, type1=O2,
type2=H2S, type3=CO. CO₂ присутствует среди поддерживаемых типов
(type15), но не в исходной конфигурации четырёх слотов. Название EX
само по себе не определяет физическую модель установленного горючего
сенсора или его состав газа калибровки. Программное расписание восстановлено
в timing.md; реальные задержки и частота ADC на плате не измерены.
Воспроизведение вычислительных проверок:
wsl.exe -d Debian -- /tmp/gas-re-venv/bin/python <PROJECT>/docs/reverse-engineering/measurement/emulate_measurement.py --binary /mnt/c/dump/fw_128k.bin --startup <PROJECT>/docs/reverse-engineering/evidence/startup_ram.bin --output <PROJECT>/docs/reverse-engineering/measurement/emulation_results.json
Для нелинейной и температурных формул разрешено расхождение 1 единица из-за
различий промежуточного float/double округления; остальные проверки точные.
Выбранные файлы первоначальной декомпиляции, которые стоит сохранять для
повторной проверки: 00000f5c.c, 000011c0.c, 0000c6f8.c, 0000fe48.c,
0001044c.c, 00009a7c.c, 00009dd8.c, 0000a134.c, 0000a490.c,
0000a5a0.c, 0000a6ac.c, 0000a9a4.c, 0000aec4.c, 0000e758.c,
00011478.c, 0000ff20.c, 0001056c.c. Автодекомпиляция не является
готовым исходным кодом; предупреждения о таблицах переходов и softfloat
сравнениях выше существенны.