19з. rusEFI: открытый блок управления двигателем на STM32 — датчики, CAN и диагностика
Разделы 19г–19ж смотрели на машину снаружи: сканер спрашивает блок, адаптер слушает шину, файл DBC расшифровывает чужие кадры. Здесь — взгляд изнутри. rusEFI — открытая прошивка блока управления двигателем (ЭБУ): она ловит зубья коленвала, считает обороты, читает датчики, решает, сколько топлива впрыснуть и когда поджечь смесь, и сама отвечает на запросы OBD по CAN. Работает на STM32F4, F7 и H7.
Это экскурсия по настоящему коду, а не пересказ. Что на каких платах работает, как устроен декодер колеса 60-2, как термистор превращается в градусы, что уходит в CAN и как rusEFI отвечает сканеру из раздела 19г. Три куска прошивки — декодер, OBD-II и термистор — собраны на ПК без правок и прогнаны на синтетическом колесе.
Опоры: исходники rusEFI — github.com/rusefi/rusefi (коммит
79ceeb32). Лицензия: GPL-3.0 с тремя оговорками
в license.txt (прошивка не для дорог общего пользования, не соответствует
экологическим нормам, не для пилотируемой авиации). Код здесь пересказан,
цитаты — по нескольку строк с путём к файлу в репозитории. Прошивка целиком
здесь не собиралась (без подмодулей ChibiOS, googletest и libfirmware):
на компьютере собраны три её части — декодер, OBD-II и термистор — с заглушками
вместо остального.
Как это называется в документации
Оригинал Перевод ECU (engine control unit) блок управления двигателем, ЭБУ trigger wheel, crank / cam задающее колесо коленвала / распредвала 60-2, 36-1 колесо на 60 (36) позиций, 2 (1) зуба сняты sync gap ratio отношение длительностей соседних зубов, «зазор» instant RPM мгновенные обороты по последним 90° VE (volumetric efficiency) table таблица наполнения speed density расчёт воздуха по давлению и температуре во впуске ignition advance угол опережения зажигания, УОЗ CLT, IAT температура охлаждающей жидкости, воздуха на впуске MAP, TPS давление во впуске, положение дросселя wideband O2, lambda широкополосный датчик кислорода, λ rev limiter, limp mode ограничитель оборотов, аварийный режим tuning software (TunerStudio) программа настройки блока
19з.1 Что это и на каком STM32 работает
Код прошивки — в firmware/. Главное для нас:
| Каталог | Что там |
|---|---|
firmware/config/boards/ |
платы: выводы, тип МК, свои настройки |
firmware/hw_layer/ports/stm32/ |
порт на STM32: stm32f4, stm32f7, stm32h7, АЦП, CAN, таймеры |
firmware/controllers/trigger/ |
декодер колеса, обороты |
firmware/controllers/sensors/ |
датчики и пересчёт напряжения в величины |
firmware/controllers/algo/ |
топливо, зажигание, настройки по умолчанию |
firmware/controllers/can/ |
CAN: панели, λ-модуль, OBD-II, ISO-TP |
firmware/console/binary/ |
связь с TunerStudio |
Каждая собираемая плата описана файлом meta-info*.env с полем
PROJECT_CPU. Таких рабочих описаний 47 (отключённые с disabled
в имени не считаются): 26 — на STM32F4, 17 — на STM32F7, 4 — на STM32H7.
Отладочные платы тоже есть: f407-discovery, nucleo_f767,
nucleo_h743. Семейства F4/F7 делят общий mcuconf_common_f4_f7.h.
Под прошивкой — ChibiOS: подмодуль firmware/ChibiOS (ветка
stable_21.11_…rusefi… в .gitmodules). Потоки, очереди CAN и драйверы
периферии — оттуда. Время прошивка ведёт в своих тактах: в
hw_layer/ports/stm32/port_mpu_util.h задан US_TO_NT_MULTIPLIER 4,
то есть 4 такта на микросекунду. Комментарий там объясняет выбор: 4 МГц —
общий делитель частот таймера TIM5 на поддерживаемых МК (84, 108 и 200 МГц).
19з.2 Двигатель за пять минут: колесо, обороты, топливо, зажигание
Чтобы читать код, хватит четырёх понятий.
Колесо. На коленвале — зубчатое колесо, датчик (индуктивный или Холла) даёт импульс на каждый зуб. Колесо 60-2 — это 60 позиций по 6°, из которых две подряд без зуба. Этот пропуск и есть метка: первый зуб после него — точка отсчёта оборота. У 36-1 шаг 10°, снят один зуб.
Обороты. Пока колесо крутится ровно, между зубьями одно и то же время.
Средние обороты rusEFI считает раз за цикл двигателя (720°, два оборота
коленвала) в firmware/controllers/engine_cycle/rpm_calculator.cpp:
60 * mult / periodSeconds, где mult = 720 / 360 = 2. Мгновенные — по
последним 90° в instant_rpm_calculator.cpp: угол в градусах, время
в тактах 4 МГц.
Топливо. По умолчанию воздух в цилиндре оценивается по давлению во
впуске (MAP), температуре воздуха и таблице наполнения
(controllers/algo/airmass/speed_density_airmass.cpp: «ideal-gas law»).
Масса топлива — от массы воздуха и нужной λ, длительность импульса
форсунки — getInjectionDuration() в controllers/algo/fuel/injector_model.cpp:
время по производительности форсунки плюс мёртвое время открытия.
Зажигание. Угол опережения — из таблицы ignitionTable по оборотам
и нагрузке (getRunningAdvance() в controllers/algo/ignition/ignition_state.cpp),
дальше поправки по температурам.
19з.3 Декодер колеса: автомат в trigger_decoder.cpp
Сердце — TriggerDecoderBase::decodeTriggerEvent() в
firmware/controllers/trigger/trigger_decoder.cpp. На каждый фронт он:
- Считает длительность зуба — разность с прошлым фронтом, в тактах;
хранит последние длительности в
toothDurations[]. - Если колесо с меткой, делит текущую длительность на прошлую — это
triggerSyncGapRatio— и зовётisSyncPoint(). - На точке синхронизации сверяет число зубов с ожидаемым. Совпало — «синхронизирован», индекс в 0. Не совпало, а синхронизация уже была — ошибка, счётчик ошибок +1, синхронизация сброшена.
- Если зубов больше, чем в обороте, а метки нет — тоже сброс.
- Если с прошлого фронта прошло больше секунды — двигатель стоял, синхронизации нет.
isSyncPoint() проверяет сразу несколько последних зазоров, каждый в своём
окне, и делает это без деления:
// firmware/controllers/trigger/trigger_decoder.cpp (rusEFI, GPL-3.0)
bool isGapCondition =
(toothDurations[i] > toothDurations[i + 1] * from
&& toothDurations[i] < toothDurations[i + 1] * to);
Окна задаёт описание колеса. initializeSkippedToothTrigger() в
decoders/trigger_universal.cpp для колеса с пропуском ставит первый зазор
в skippedCount + 1 (у 60-2 — 3, у 36-1 — 2), второй — в 1, а
TRIGGER_GAP_DEVIATION 0,25f из trigger_structure.h расширяет каждый на
±25 %. Колёса 60-2 и 36-1 объявлены с SyncEdge::RiseOnly: учитываются
только подъёмы.
Прогон. Стенд собрал trigger_decoder.cpp g++ 12.2 в stm-sys:1 вместе
с настоящими trigger_decoder.h и двумя файлами состояния из
live_data_generated/. Колесо заполнено по правилам выше, фронты подаются
с шагом тактов 4 МГц, пуск — с десятого зуба.
| Колесо | Событий в описании | Подъёмов | Окна зазоров |
|---|---|---|---|
| 60-2 | 116 | 58 | [2,25; 3,75] и [0,75; 1,25] |
| 36-1 | 70 | 35 | [1,5; 2,5] и [0,75; 1,25] |
| Колесо, об/мин | Первая синхронизация | Зазор | Обороты по циклу | Мгновенные |
|---|---|---|---|---|
| 60-2, 150 | 49-й зуб | 3,000 | 150,00 | 150,00 |
| 60-2, 800 | 49-й зуб | 3,000 | 800,00 | 800,00 |
| 60-2, 3000 | 49-й зуб | 3,001 | 3000,00 | 3000,00 |
| 60-2, 6500 | 49-й зуб | 2,997…3,003 | 6500,01 | 6499,84 |
| 36-1, 3000 | 31-й зуб | 1,999 | 3000,00 | 3000,00 |
49-й зуб — это 48 зубьев до пропуска плюс первый после него: раньше метки декодер не знает, где он. За семь оборотов — 7 синхронизаций и ни одной ошибки. Индекс после синхронизации растёт на 2 (подъём и пропущенный спад) и доходит до 114. Отклонение зазора и сотых долей оборота на 6500 об/мин — округление времени зуба (154 мкс) до такта 0,25 мкс.
Сбои, 60-2 на 3000 об/мин:
- Пропал зуб (событие 300): на ближайшей метке (событие 339) зубов 57 вместо 58 — ошибка, счётчик ошибок 1, синхронизация сброшена. Следующая метка (397) — снова синхронизирован.
- Помеха посреди зуба (событие 300): лишний фронт сдвигает счёт, индекс доходит до конца оборота раньше метки — на событии 338 ошибка и сброс, а метка на 339 сразу же синхронизирует снова.
- Разгон 1000 → 7000 об/мин за 1 с: зазор ушёл до 2,977…3,004, 70 синхронизаций, ошибок 0.
- Двигатель встал — пауза 1,5 с: первый же зуб после неё приходит без синхронизации.
Ошибка «зубов больше, чем надо» считается, только если обороты уже не 0: в коде так и написано — «let's not show a warning if we are just starting to spin». На раскрутке сбой лишь сбрасывает синхронизацию.
19з.4 Датчики: термистор, напряжение и отказ
Температурные датчики — термисторы с отрицательным коэффициентом
в делителе с подтягивающим резистором. Цепочка пересчёта собрана из двух
функций (controllers/sensors/converters/):
ResistanceFunc(resistance_func.cpp) — напряжение в сопротивление: ниже 0,05 В — замыкание (Low), выше 98 % питания — обрыв (High), иначеR = Rподт / (Uпит / U − 1).ThermistorFunc(thermistor_func.cpp) — уравнение Стейнхарта — Харта1/T = A + B·ln R + C·(ln R)³. Коэффициенты A, B, Cconfigure()вычисляет по трём точкам «температура — сопротивление» из настроек; ниже −50 °C и выше 250 °C (CLT_UPPER_VALIDATION_LIMIT) — отказ.
Датчик охлаждающей жидкости по умолчанию
(controllers/algo/engine_configuration.cpp): 0, 23,8889 и 48,8889 °C
при 9500, 2100 и 1000 Ом, подтяжка 1500 Ом. Прогон тех же двух файлов
на ПК, питание делителя 5 В:
| Вход | Сопротивление | Температура |
|---|---|---|
| — | 9500 Ом | 0,00 °C |
| — | 2100 Ом | 23,89 °C |
| — | 1000 Ом | 48,89 °C |
| — | 5000 Ом | 6,39 °C |
| 1,00 В | 375,0 Ом | 103,10 °C |
| 2,50 В | 1500,0 Ом | 33,90 °C |
| 4,50 В | 13500,0 Ом | −1,33 °C |
| 0,02 В | — | отказ Low (замыкание) |
| 4,95 В | — | отказ High (обрыв) |
Коэффициенты вышли A = −1,592602·10⁻³, B = 8,205510·10⁻⁴,
C = −2,943859·10⁻⁶. Три опорные точки воспроизводятся точно; всё, что
горячее 48,9 °C, — экстраполяция: 300 Ом дают 120,34 °C, 150 Ом — 192,28 °C.
Датчик воздуха (setCommonNTCSensorParameters(): −20, 23,8889 и 120 °C при
18000, 2100 и 100 Ом) опирается на точки пошире; его три точки на стенде
дали −20,00, 23,89 и 120,00 °C.
MAP и дроссель — линейные датчики (linear_func.cpp: две точки
«напряжение — величина»). λ берётся по CAN с отдельного модуля (19з.5).
Отказ датчика — не число, а код (UnexpectedCode); дальше аварийный
режим сам решает, что с ним делать (19з.7).
19з.5 CAN: панели, λ-модуль и TunerStudio
Вся работа с CAN — в firmware/controllers/can/.
Передача. can_tx.cpp раз в 5 мс проходит по циклу и выбирает, что
пора слать: интервалы 10, 20, 50, 100, 200, 250, 500 и 1000 мс. Данные
для приборной панели — updateDash() в can_dash.cpp: протокол выбирает
настройка canNbcType, кроме «нет» — 13 вариантов: BMW E46 и E90, VAG и
VAG MQB, Fiat, Mazda RX-8, Mercedes W202, Nissan VQ, Hyundai Genesis Coupe,
Honda K, Haltech, панели AiM и простая рассылка в формате MegaSquirt. Свой
подробный набор кадров rusEFI описан в rusEFI_CAN_verbose.dbc — его
читают средствами из раздела 19е.
Приём. processCanRxMessage() в can_rx.cpp раздаёт кадр по
слушателям: OBD, UDS, скрипты Lua, модуль λ. Широкополосный λ-модуль
rusEFI шлёт на стандартных идентификаторах: у первого датчика 0x190 —
данные, 0x191 — диагностика (AemXSeriesLambda.cpp, «0th sensor is 0x190
and 0x191»). λ приходит целым числом с шагом 0,0001. Датчики AEM X-Series —
на расширенных идентификаторах начиная с 0x180, тот же шаг 0,0001.
UDS. uds.cpp — всего 28 строк: на 0x7E0 блок понимает одну службу 22
с идентификатором F1A4 и отвечает на 0x7E8 четырьмя буквами rEFI; на
неверную длину — отказ 7F 22 13. Это визитная карточка для программ, а не
полноценный UDS из раздела 19д.
TunerStudio. Программа настройки говорит с блоком по UART, USB (CDC)
или CAN. Протокол — однобайтовые команды (S — приветствие, O —
текущие значения, R — чтение, C — запись куска настроек, B —
сохранить во флеш) и CRC32 в конце пакета (tunerstudio_io.cpp). По CAN
тот же поток идёт через ISO-TP (раздел 19в): controllers/can/isotp/isotp.h
знает четыре типа кадров — одиночный, первый, последующий, управление
потоком; идентификаторы — CAN_ECU_SERIAL_RX_ID 0x710 и
CAN_ECU_SERIAL_TX_ID 0x720 (плата f407-discovery).
19з.6 OBD-II в rusEFI: сверка с разделом 19г
obd2.cpp — 227 строк: блок слушает общий запрос 0x7DF и отвечает с 0x7E8,
одним кадром из 8 байт — длина, 0x40 + служба, PID, данные. Стенд собрал
файл без правок, подставил значения датчиков и прогнал запросы через
obdOnCanPacketRx():
| Запрос | Ответ rusEFI | По формуле 19г | Было в датчике |
|---|---|---|---|
| 01 0C | 04 41 0C 2E E0 | (256·0x2E + 0xE0)/4 = 3000 об/мин | 3000 |
| 01 05 | 03 41 05 82 | 0x82 − 40 = 90 °C | 90 |
| 01 0F | 03 41 0F 39 | 0x39 − 40 = 17 °C | 17 |
| 01 0B | 03 41 0B 26 | 38 кПа | 38 |
| 01 0D | 03 41 0D 3C | 60 км/ч | 60 |
| 01 11 | 03 41 11 2B | 0x2B·100/255 = 16,863 % | 16,863 |
| 01 10 | 04 41 10 18 1F | 0x181F/100 = 61,75 г/с | 61,75 |
| 01 42 | 04 41 42 39 4B | 0x394B/1000 = 14,667 В | 14,667 |
| 01 04 | 03 41 04 80 | 0x80·100/255 = 50,196 % | 50 |
| 01 0E | 03 41 0E 94 | 0x94/2 − 64 = 10° | 10° |
Формулы совпали с разделом 19г. Округление — efiRound() до целого, затем
обрезка в 0…255 или 0…65535: 16 000 об/мин ушли как FA 00, 20 000 — как
FF FF (16383,75), −50 °C — как 00 (−40 °C).
Карты поддержки. На 0100 блок ответил BD 3E 80 00 — 12 PID:
01 03 04 05 06 08 0B 0C 0D 0E 0F 11. На 0120 — 10 00 00 00 (только 0x24,
отношение λ), на 0140 — 40 00 40 14 (0x42, 0x52, 0x5C, 0x5E). Всего
в трёх картах 17 PID. Две странности видны сразу:
- Младший бит карты
0100(PID 0x20 — «спроси следующую карту») не выставлен, как и в0120. Сканер, который идёт по цепочке карт, как в 19г, дальше0100не пойдёт и не узнает про напряжение и масло. - На PID 0x10 (расход воздуха) блок отвечает, но в карте его нет.
На 012F и 01A6 — тишина, как описано в 19г. Службы 03 и 07 тоже
молчат: handleDtcRequest() пуста, в коде стоит «TODO: this appears to be
unfinished?». Коды ошибок rusEFI показывает в TunerStudio, а не сканеру.
19з.7 Грабли
Прошивка из копии не собирается. Подмодули ChibiOS, libfirmware,
googletest и прошивка λ-модуля в копии пусты: .gitmodules на месте,
каталоги — нет. Нужен git submodule update --init; без сети остаётся
собирать отдельные файлы с заглушками, как здесь.
Колесо не синхронизируется на VR-датчике. Индуктивный датчик даёт оба
фронта, а колесо 60-2 объявлено как RiseOnly. Если спады дойдут до
декодера, он посчитает их «неважными» и сдвинет индекс — оборот не
сойдётся. Фронты фильтрует уровень выше (trigger_central.cpp), в стенде
подаются только подъёмы.
Синхронизация теряется на раскрутке, а ошибок 0. Лишний зуб при нулевых оборотах не считается ошибкой — только сбрасывает синхронизацию. Смотреть надо и на счётчик ошибок, и на число потерь синхронизации.
Термистор врёт в горячем. Три точки CLT по умолчанию кончаются на 48,9 °C; при 90 °C уравнение уже экстраполирует. Точки надо брать из паспорта своего датчика по всему рабочему диапазону.
Сканер видит только часть PID. См. 19з.6: без бита 0x20 в 0100
цепочка карт обрывается. Для своего блока карту поддержки строят с битом
следующей карты.
Ограничитель и аварийный режим. limp_manager.cpp решает, можно ли
лить топливо и давать искру. По умолчанию (default_base_engine.cpp)
отсечка на 7000 об/мин, возврат на 50 об/мин ниже, режутся и топливо,
и искра. Причин запрета 22 (ClearReason в limp_manager.h): отсечка,
давление масла, заедание дросселя (тогда предел 1500 об/мин), фатальная
ошибка (предел 0), нет фазы двигателя и другие. Код причины выводится
в TunerStudio — начинать поиск «почему не заводится» с него.
19з.8 Итоги
- rusEFI — ЭБУ на STM32 под ChibiOS, GPL-3.0: 47 рабочих описаний плат, из них 26 на F4, 17 на F7 и 4 на H7; время — такты 4 МГц.
- Декодер — автомат из двух состояний: колесо 60-2 — 116 событий, 58 подъёмов, окна зазоров [2,25; 3,75] и [0,75; 1,25]. На 150, 800, 3000 и 6500 об/мин синхронизация на 49-м зубе с пуска, 0 ошибок; обороты по циклу 6500,01, мгновенные 6499,84.
- Пропавший зуб даёт 1 ошибку и один оборот без синхронизации; помеха — сброс и синхронизацию на следующей метке; разгон 1000 → 7000 об/мин за 1 с — 0 ошибок; пауза больше 1 с — синхронизации нет.
- Термистор — Стейнхарт — Харт по трём точкам; CLT по умолчанию: 9500, 2100, 1000 Ом → 0,00, 23,89, 48,89 °C; ниже 0,05 В — замыкание, выше 98 % питания — обрыв.
- CAN: 13 протоколов панелей, λ на 0x190/0x191 с шагом 0,0001, TunerStudio по ISO-TP на 0x710/0x720, UDS — одна служба 22 F1A4.
- OBD-II: формулы совпали с 19г (3000 об/мин →
2E E0); в трёх картах 17 PID, бита следующей карты нет, службы 03 и 07 молчат. - Отсечка по умолчанию 7000 об/мин, 22 причины запрета топлива и искры.
Дальше: раздел 19и «J1939: CAN грузовиков и спецтехники — PGN, SPN и диагностика DM1» — как та же шина CAN работает на грузовиках и спецтехнике: 29-битные адреса, PGN и коды DM1.