Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LC-1: восстановленная прошивка

Исходный код на C для контроллера широкополосного лямбда-зонда Innovate Motorsports LC-1, версия 1.20 RC4. МК ATmega64A, 16 МГц, образ 28 672 байта.

Это реконструкция: исходников производителя в открытом доступе нет, код восстановлен из двоичного образа сверкой с дизассемблером. Прибор снят с производства, официальной поддержки у него не осталось, а приборы живы - отсюда и затея.

Состояние

Восстановлено всё, заглушек нет. Из 20 710 байт кода 16 944 (81.8 %) переписаны на C, оставшиеся 3 766 - рантайм avr-gcc (плавающая точка, 32-битные умножение и деление, прологи-эпилоги); переписывать его незачем, avr-libc даёт то же самое. Формальная проверка полноты: в образе 331 цель вызова помимо рантайма, и все 331 разобраны.

На живом приборе код не проверялся. Ниже есть список того, что стоит подтвердить перед тем, как что-то на нём запускать.

Сборка Код Расхождение с оригиналом
оригинал 20 710
avr-gcc 3.4.3, WinAVR-20050214 19 916 -3.8 %
avr-gcc 14.3 17 100 -17 %

Побайтного совпадения нет и быть не может: компилятор восстановлению не подлежит вплоть до конкретного билда. Сверка велась по смыслу и последовательности операций. Косвенный признак того, что структура угадана верно: у сборки старым компилятором таблица векторов совпадает с оригиналом точно - те же девять занятых векторов и ни одного лишнего. Это проверяется автоматически при каждой сборке.

Сборка

Эталонная сборка - компилятором той же эпохи. Нужен только docker:

sh tools/build-oldgcc.sh

Поднимется контейнер Debian sarge с avr-gcc 3.4.3 и binutils 2.15 (то же, что в WinAVR-20050214), результат ляжет в build-oldgcc/: .elf, .hex, полный дизассемблер .lst, размеры, список векторов. Образ для прибора берут отсюда.

Современный avr-gcc годится для проверки на чистую компиляцию и для отладки, но раскладывает код иначе и подставляет свой, более новый рантайм:

make            собрать build/lc1.hex и build/lc1.eep
make size       размер секций
make disasm     дизассемблер собранного
make clean
make flash      avrdude, см. переменные PROGRAMMER и PORT в Makefile

Windows без make: build.bat. Нужен avr-gcc в PATH - подойдёт WinAVR, тулчейн из Microchip Studio или сборка с github.com/ZakKemble/avr-gcc-build. Если его там нет, в начале файла есть закомментированная строка set PATH=....

Microchip Studio (бывшая Atmel Studio), если нужен отладчик:

  1. File -> New -> Project -> GCC C Executable Project, устройство ATmega64.
  2. Удалить созданный main.c.
  3. Правой кнопкой на проекте -> Add -> Existing Item -> добавить все файлы из src\ и оба заголовка из include\ (Add As Link, чтобы не плодить копии).
  4. Project -> Properties -> Toolchain:
    • AVR/GNU C Compiler -> Directories: добавить ../include;
    • AVR/GNU C Compiler -> Optimization: -Os;
    • AVR/GNU C Compiler -> Miscellaneous: -std=gnu99.
  5. Симулятор в Studio для ATmega64 работает, но аналоговый компаратор и АЦП придётся стимулировать вручную - вся измерительная логика висит на них.

include/compat_oldavr.h подключается принудительно (-include) и нужен старой avr-libc: макроса ISR() до версии 1.4 не существовало, обработчики объявлялись как SIGNAL(SIG_...). Если avr-libc уже знает ISR(), файл сворачивается в пустое место, так что мешать современной сборке он не может.

Сборка в CI и релизы

.github/workflows/build.yml на каждый push собирает прошивку обоими компиляторами и сверяет набор векторов прерываний с эталонным списком tools/expected-vectors.txt. Современная сборка идёт с -Werror.

Релиз делается вручную: Actions -> build -> Run workflow, в поле тега указать версию (например v1.20rc4-r1). Пустое поле - просто сборка без релиза. В релиз кладутся оба образа: lc1-oldgcc.* (эталонный) и lc1.* (современный), с дизассемблерами и размерами.

Загрузчик: прочитайте до прошивки

В образе, с которого сделана реконструкция, загрузчика нет - и это ожидаемо: образ взят из файла обновления прошивки, а тот загрузчик не содержит по определению, обновление как раз через него и идёт. Заводской bootloader живёт в boot-секции по адресу слова 0x7E00 (последние 1024 байта flash); сервисная команда 'P' передаёт на него управление.

Отсюда следствия:

  • avrdude с полным стиранием кристалла уничтожит загрузчик, и обновлять прибор штатным способом станет нельзя. Сохраните дамп до записи: avrdude -p m64 -c <прогр.> -U flash:r:backup.bin:r
  • Фьюзы (в частности BOOTRST и размер boot-секции) в этом проекте не заданы и записываться не должны. make fuses только печатает команду чтения.
  • EEPROM хранит калибровку, имя прибора и настройки аналоговых выходов. При первом запуске прошивка сама инициализирует её значениями по умолчанию, если по адресу 0 нет признака 0x5A.

Что внутри

src/                    33 файла, разбиение повторяет структуру оригинала
include/lc1.h           регистры, глобальные переменные с адресами ОЗУ, прототипы
include/compat_oldavr.h слой совместимости с avr-libc 2005 года
tools/build-oldgcc.sh   эталонная сборка в контейнере
tools/oldgcc/           образ с avr-gcc 3.4.3
tools/ghidra/           скрипты и processor spec для разбора образа
Makefile                сборка через GNU make
build.bat               сборка в Windows без make
.clang-format           стиль (120 колонок)
Файлы О чём
nernst_ac.c, cell_measure.c, adc.c, adc_isr.c автоколебательный контур накачки ячейки, замеры T1/T2, Rpump, Rnernst
lambda_calc.c duty cycle, lambda, %O2
heater*.c сопротивление нагревателя, ПИ на прогреве, ПИД в работе, калибровка
state_machine.c 12 состояний прибора
air_cal.c, eeprom_cfg.c калибровка по свободному воздуху, конфигурация
analog_out.c, dac.c два аналоговых выхода через AD5312
proto_frame.c, command_task.c, protocol_relay.c, service_cmd.c протокол Innovate MTS (ISP2)
uart.c, tick_isr.c, led.c, util_delay.c, lib.c нижний уровень
vars_*.c глобальные переменные, сгруппированные по блокам адресов ОЗУ

Как это работает

Память. .data занимает ОЗУ 0x0100..0x0209 (266 байт), .bss - 0x020A..0x03B9 (432 байта), стек растёт вниз от 0x10FF. Из 35 векторов заняты девять: TIMER2_OVF (системный тик), TIMER1_OVF, SPI_STC, USART0_RXC, USART0_UDRE, ADC, ANA_COMP, USART1_RXC, USART1_UDRE.

Тактирование. АЦП 125 кГц (/128), опора AVCC, 1 LSB = 4.88 мВ. Timer2: Fast PWM /64 - ШИМ нагревателя 976.56 Гц и тик переполнения 1.024 мс; счёт двадцати тиков даёт основной период 20.48 мс. Timer1: /8, 0.5 мкс на тик - им меряются полупериоды колебаний. SPI: 8 МГц, CPOL=0, CPHA=1.

Автоколебательный контур - самое нетривиальное место. Ток накачки фиксирован по величине и переключается только по знаку; напряжение на ячейке Нернста линейно ползёт, и как только пересекает опору, срабатывает аналоговый компаратор МК. Его обработчик защёлкивает 32-битную метку времени {TCNT1, переполнения}, ждёт 3.1 мкс и перепроверяет ACO - программный антидребезг, - после чего переворачивает знак тока, меняет чувствительный фронт компаратора и берёт отсчёт для цепи вычитания помехи (ниже). Длительности полуволн T1 и T2 запоминаются раздельно.

Целиком это однобитный сигма-дельта модулятор: ячейка Нернста работает интегратором, компаратор - квантователем, линия DIR_CONTROL - однобитной обратной связью, а скважность и есть цифровой результат. Поэтому прошивке достаточно T1 и T2, и никакой точный ЦАП тока накачки не нужен - хватает двух резисторов и ключа, а ответ выходит временным отношением, которое таймер меряет с шагом 0.5 мкс. Это измеритель лямбды, а не температуры: ни период, ни сумма полуволн нигде не превращаются в сопротивление.

Величину тока задаёт U3B: на его выходе 4.01 В при DIR_CONTROL = 0 и 0.99 В при 1, то есть перепад +/-1.51 В на резисторе, который выбирает ключ U2 по типу датчика: R6 243 Ом для Bosch LSU (около 5.5 мА) и R5 182 Ом для NTK (около 7.1 мА). Усилитель U1A держит общий электрод LSU_COM на 2.5 В, поэтому весь ток идёт через ячейку накачки.

Цепь вычитания помехи (на схеме этот блок подписан NERNST_CONTROL). Ток накачки возвращается через тот же вывод датчика, относительно которого меряется напряжение Нернста - Bosch называет его "Virtual Ground". Всё, что лежит между внутренней точкой соединения ячеек и клеммой, несёт полный ток накачки, и падение на этом участке входит в измеряемую разность. Операционник удерживает клемму, а не точку внутри керамики, поэтому убрать это регулятором нельзя.

Чисто резистивная часть такой помехи в обе стороны одинакова и на скважность не влияет: она входит множителем в оба времени сразу и из отношения (T1-T2)/(T1+T2) сокращается. Мешает другая часть - перенос кислорода через границу платина/оксид циркония идёт в две стороны с разной кинетикой, зависящей от температуры и от самой лямбды. Её нельзя вычислить, только измерить, причём отдельно для каждого направления.

Схема так и делает. Выход усилителя ячейки через C1 0.01 мкФ на R14 10 кОм (фильтр верхних частот, срез 1.59 кГц - медленная ЭДС Нернста не проходит) идёт на инвертор с коэффициентом -1. Ключ U4A по команде AC_CENT_ENABLE подключает его к конденсатору хранения, а какой именно - выбирает ключ U5A линией DIR_CONTROL, то есть у каждого направления свой конденсатор. Повторитель отдаёт запомненное как V_OFFSET, и оно подмешивается в узел VN_COMP_CENTER перед компаратором.

Отсюда и порядок в обработчике: перевернуть ток, выждать 3.125 мкс (за это время химия ячейки измениться не успевает, а электрический скачок уже произошёл), замкнуть ключ на 1.25 мкс, разомкнуть. Окно выборки намного короче постоянной времени заряда (два ключа по 30 Ом на 1 мкФ, около 60 мкс), поэтому за отсчёт переносится 1...2 % от цели: фактически это усреднитель с постоянной времени в несколько десятков полупериодов. Предварительный заряд конденсаторов делает состояние "раскачка ячейки" - там та же последовательность повторяется 50 раз с увеличенным до 6.25 мкс окном, что и объясняет это состояние.

Гистерезис компаратора задан отдельно и резисторами: PA3 через R24 8.25 кОм сдвигает опору на +/-44 мВ вокруг 2.95 В, и переключается эта линия всегда вместе с DIR_CONTROL.

Измерение лямбды. Из того же обработчика запускается АЦП на канале VPUMP_SENSE, и дальше работает маленький автомат в прерывании АЦП: два замера подряд с переключением направления накачки между ними, результат - их разность, чем компенсируется смещение тракта. Дальше DC = (T1 - T2) / (T1 + T2), lambda = DCair / (DCair - DC), а %O2 * 10 = DC / DCair * 209 + 6.

Сопротивления ячеек меряются отдельно и синхронно, функцией cell_measure. Для ячейки Нернста: PA1..PA3 переводятся во входы, затем PA2 RN_MEAS_PULSE делается выходом, снимается отсчёт RN_SENSE, подаётся импульс нагрузки, снимается второй отсчёт, результат - (v_high - v_low) * 8 / 7. То есть падение напряжения при известном токе. Импульс подаётся через R21 10 кОм, и амплитуда тока выходит ровно +/-250 мкА - в точности паспортный предел Bosch на измерение Ri. Сквозная проверка шкалы сходится: при усилении тракта 8.55 и шаге АЦП 4.88 мВ множитель 8/7 даёт результат в омах с погрешностью 0.06 %. Для ячейки накачки - то же самое через VPUMP_SENSE и переключение DIR_CONTROL. В рабочем режиме Нернста так не меряют: подача тока сбила бы автоколебания.

Нагреватель. Сопротивление считается как Rheat = Ubat * 5 * 442 / Iheat и хранится в миллиомах. Регулируемая величина меняется по ходу работы:

Фаза Регулятор По чему
прогрев ПИ сопротивление самого нагревателя, уставка 9100 / 6000 / 7500 мОм по типу датчика
калибровка нагревателя ПИД сопротивление ячейки Нернста, уставка 200 для LSU 4.9, иначе 80
работа ПИД сопротивление ячейки НАКАЧКИ

Уставка 7500 для LSU 4.9 - это паспортная рабочая точка Bosch: при номинальных 7.5 В и мощности около 7.5 Вт в равновесии сопротивление горячего нагревателя равно 7.5 Ом. Порог обрыва 1800 совпадает с паспортным минимальным холодным сопротивлением 1.8 Ом.

Подмена происходит в конце калибровки: измеряется сопротивление ячейки накачки, и оно кладётся сразу и в уставку, и в измеренное значение. Смысл в том, чтобы запомнить, какое сопротивление накачки у этого конкретного датчика соответствует прогреву до целевого Rnernst; дальше прибор держит уже его. Уставка переживает выключение - она лежит в EEPROM по адресу 0x16.

Пороги диагностики (Rpump < 18 - замыкание, >= 600 - обрыв, Rnernst < 2 и >= 501) совпадают с опубликованной таблицей LC-1 1.10.

Вывод. Два аналоговых выхода через двухканальный ЦАП AD5312 по SPI: analog_out.c копит измерения и раз в заданный период выдаёт среднее, пересчитанное кусочно-линейной характеристикой из EEPROM. Умолчания: канал 0 - эмуляция узкополосного датчика, канал 1 - 0..5 В на lambda 0.50..1.523. Отрицательное значение подменяется словом 0x3000: это PD1 = PD0 = 1, "выключить канал, выход в третьем состоянии" - так гасятся выходы на прогреве и при ошибке. AD5312 десятиразрядный, данные занимают D11..D2, хотя прошивка везде считает в шкале 0..4095.

Протокол Innovate MTS (ISP2), 19200 8N1: заголовок - слово, у обоих байт которого установлен бит 7, длина в словах; байты данных бит 7 не имеют, значение собирается из двух семибитных половин. Восстановлен целиком, включая ретрансляцию пакетов от других приборов в цепочке и служебный ("out-of-band") режим.

Известные дефекты оригинальной прошивки

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

  1. cell_measure() вызывается без аргумента (state_machine.c, ветка для LSU 4.2). В регистре остаётся младший байт только что измеренного Rnernst, поэтому номер режима получается случайным. Симметричная ветка для LSU 4.9 аргумент задаёт явно.
  2. Сервисная команда 'K' (задать тип датчика) подвешивает прибор (service_cmd.c). Сравнение имён идёт функцией memcmp_lc1, у которой в цикле не меняются ни указатели, ни счётчик: при совпадении первых байт она не возвращается. Все три сравнения именно такие. Рядом в прошивке есть исправная восьмибайтовая сверка str_eq_8 - видимо, её и следовало вызывать.
  3. Команда 'Z' (сброс калибровки) чистит EEPROM не полностью (service_cmd.c): адрес 0x0E обнуляется трижды, а 0x10 не трогается, хотя калибровка по воздуху лежит в паре 0x0E/0x10. После "сброса" половина её остаётся в памяти.

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

Что стоит проверить на приборе

Четыре величины выведены из ASM однозначно, но с внешними описаниями расходятся.

Что В прошивке Ожидалось Чем проверить
Уставка Rnernst для LSU 4.9 200 300 Ом по паспорту Bosch паспортное значение снято переменным током 1..4 кГц, а прошивка меряет импульсом постоянной нагрузки; снять осциллографом импульс на PA2 и сопоставить
Темп разгона нагревателя ступени +100, затем +6 единиц; уровни 7.00 и 12.99 В совпадают с паспортом не быстрее 0.4 В/с осциллограф на затвор Q1: время подъёма с 7 до 13 В, по паспорту около 15 с
Смещение в формуле %O2 +0.6 +0.08 по опубликованному описанию калибровка по воздуху, показание должно дать 20.9 %
Шаг delay_ms() 0.5 мс 1 мс по имени функции осциллограф на PB1 при старте: импульс 25 мс подтвердит, 50 мс опровергнет
Вывод PB5 / TERM_DETECT не используется нигде по схеме подтянут к +5 В через R4 вопрос к схеме, а не к коду

Множитель 442 в расчёте Rheat из этого списка выбыл: чтобы результат получался в миллиомах, он должен равняться 1000 * 0.05 * G, откуда усиление токового канала G = 8.84. Прежняя оценка "около 500" исходила из круглого x10.

Ещё одна мелочь без последствий: main пишет SFIOR = 0x10, а у ATmega64 бит 4 этого регистра зарезервирован. Наблюдаемого эффекта нет; похоже на перенос кода с другого МК.

Как это восстанавливалось

Единственным надёжным источником считался дизассемблер. Декомпилятор Ghidra использовался как черновик и на веру не принимался: из имён, которые он расставил сам, FID-меткой (Library Function - Single Match) подтверждены ровно восемь функций, и все восемь - из библиотеки компилятора. Всё остальное - догадки, и часть из них оказалась неверной. Например, функция, названная memcmp, никаким memcmp не является: у неё в цикле не двигаются указатели (см. дефект 2 выше), а memcpy на деле memcpy_P, потому что читает флеш через lpm.

Разбиение на файлы восстановлено по адресам глобальных переменных: компоновщик кладёт .bss и .data каждого объектного файла подряд, поэтому непрерывный блок адресов ОЗУ - это переменные одного .c из оригинального проекта. Отсюда и группировка vars_*.c.

Ничего не "оптимизировалось" относительно ASM: если оригинал читает переменную дважды, в коде тоже два чтения (через volatile), потому что иначе теряется соответствие. Формулы и константы там, где это было возможно, сверялись с внешними описаниями прибора: пороги диагностики, таблица типов датчика, коэффициенты регуляторов и расчёт лямбды совпали независимо.

Чего в этом репозитории нет: двоичного образа, дизассемблера и рабочих материалов сверки - в них у каждой функции проставлен диапазон адресов code:XXXX, по которому работу можно проверить построчно. Это отдельный архив.

Внешние документы

  • "Innovate Serial Protocol 2 (ISP2) specification (preliminary)", 5/3/2004
  • "Serial2 Protocol Supplement", 9/21/2006
  • Analog Devices AD5302/AD5312/AD5322, Rev. D
  • Bosch LSU 4.9, Technical Product Information Y 258 E00 015e, 14.06.2005
  • LC-1 Manual, схема прибора

Правовой статус

Прошивка LC-1 - произведение Innovate Motorsports. Здесь нет ни их двоичного образа, ни их исходников: это самостоятельно написанный код, воспроизводящий поведение прибора, чтобы им можно было продолжать пользоваться и чинить его. Лицензия на код не объявлена намеренно - объявлять её на реконструкцию чужой прошивки было бы некорректно. Используйте на свой страх и риск; работоспособность на живом приборе не проверялась.

About

Реконструкция прошивки контроллера широкополосного лямбда-зонда Innovate Motorsports LC-1 (ATmega64A), восстановленная из двоичного образа

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages