Исходный код на 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), если нужен отладчик:
- File -> New -> Project -> GCC C Executable Project, устройство ATmega64.
- Удалить созданный
main.c. - Правой кнопкой на проекте -> Add -> Existing Item -> добавить все файлы
из
src\и оба заголовка изinclude\(Add As Link, чтобы не плодить копии). - Project -> Properties -> Toolchain:
- AVR/GNU C Compiler -> Directories: добавить
../include; - AVR/GNU C Compiler -> Optimization:
-Os; - AVR/GNU C Compiler -> Miscellaneous:
-std=gnu99.
- AVR/GNU C Compiler -> Directories: добавить
- Симулятор в Studio для ATmega64 работает, но аналоговый компаратор и АЦП придётся стимулировать вручную - вся измерительная логика висит на них.
include/compat_oldavr.h подключается принудительно (-include) и нужен
старой avr-libc: макроса ISR() до версии 1.4 не существовало,
обработчики объявлялись как SIGNAL(SIG_...). Если avr-libc уже знает
ISR(), файл сворачивается в пустое место, так что мешать современной
сборке он не может.
.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") режим.
Воспроизведены намеренно: код повторяет двоичный образ, а не то, как задумывалось. Каждый помечен комментарием в исходнике.
cell_measure()вызывается без аргумента (state_machine.c, ветка для LSU 4.2). В регистре остаётся младший байт только что измеренного Rnernst, поэтому номер режима получается случайным. Симметричная ветка для LSU 4.9 аргумент задаёт явно.- Сервисная команда
'K'(задать тип датчика) подвешивает прибор (service_cmd.c). Сравнение имён идёт функциейmemcmp_lc1, у которой в цикле не меняются ни указатели, ни счётчик: при совпадении первых байт она не возвращается. Все три сравнения именно такие. Рядом в прошивке есть исправная восьмибайтовая сверкаstr_eq_8- видимо, её и следовало вызывать. - Команда
'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. Здесь нет ни их двоичного образа, ни их исходников: это самостоятельно написанный код, воспроизводящий поведение прибора, чтобы им можно было продолжать пользоваться и чинить его. Лицензия на код не объявлена намеренно - объявлять её на реконструкцию чужой прошивки было бы некорректно. Используйте на свой страх и риск; работоспособность на живом приборе не проверялась.