Исследование / 13.09.2026

Измерительный тракт 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Использование
0CH50x200011FAТемпература
1CH60x200011F0Газовый слот 0: EX
2CH130x200011F2Газовый слот 1: O2
3CH140x200011F6Газовый слот 3: CO
4CH150x200011F4Газовый слот 2: H2S
5CH27 / VBAT÷30x200011F8Батарея

Этот порядок восстановлен из реальной таблицы переходов после вызова

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*ch57-байтная запись калибровки в RAM
0x20000EA3 + 0x13*ch19-байтная запись типа, пределов и настроек слота
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⁴
ТипАдрес коэффициентовc0c1c2c3c4
20x114F40.9442620.003837432-0.00005329840.00000011640
30x115080.771037160.0150139-0.00009775-0.000005820.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 или аккумулятора.

Что это означает для нового ядра

значение с температурной компенсацией, значение для тревог и для экрана.

В диагностическом режиме сохранять каждый этап.

отдельный ноль, 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

сравнениях выше существенны.