Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F407, STM32F767, STM32H743 · уровень продвинутый · 15 мин чтения
Открыть в интерактивном справочнике

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), дальше поправки по температурам.

Слева вверху — развёрнутое в строку колесо 60-2: подряд идут зубья с шагом 6 градусов, на месте двух позиций пропуск, первый зуб после пропуска отмечен как точка синхронизации; рядом пояснение, что зазор — отношение длительности текущего зуба к предыдущему, у обычного зуба он равен 1, у первого после пропуска 3, окно rusEFI для 60-2 от 2,25 до 3,75 и второе окно от 0,75 до 1,25, для 36-1 от 1,5 до 2,5. Ниже — автомат декодера из двух состояний, нет синхронизации и синхронизирован, переход по зазору в окне и обратно при неверном числе зубов, лишнем зубе или паузе больше секунды. Ещё ниже — цепочка блока на STM32 под ChibiOS: датчики CLT, IAT, MAP, TPS и лямбда, обороты и фаза по зубьям, таблицы топлива и угла опережения, форсунки и катушки с ограничителем 7000 об/мин и аварийным режимом. Справа — шина CAN: rusEFI шлёт данные панели по 13 протоколам, принимает лямбду с модуля на 0x190 и 0x191, отвечает сканеру на 0x7DF кадром 0x7E8 04 41 0C 2E E0, то есть 3000 об/мин, и говорит с TunerStudio по ISO-TP на 0x710 и 0x720


19з.3 Декодер колеса: автомат в trigger_decoder.cpp

Сердце — TriggerDecoderBase::decodeTriggerEvent() в firmware/controllers/trigger/trigger_decoder.cpp. На каждый фронт он:

  1. Считает длительность зуба — разность с прошлым фронтом, в тактах; хранит последние длительности в toothDurations[].
  2. Если колесо с меткой, делит текущую длительность на прошлую — это triggerSyncGapRatio — и зовёт isSyncPoint().
  3. На точке синхронизации сверяет число зубов с ожидаемым. Совпало — «синхронизирован», индекс в 0. Не совпало, а синхронизация уже была — ошибка, счётчик ошибок +1, синхронизация сброшена.
  4. Если зубов больше, чем в обороте, а метки нет — тоже сброс.
  5. Если с прошлого фронта прошло больше секунды — двигатель стоял, синхронизации нет.

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 об/мин:

Ошибка «зубов больше, чем надо» считается, только если обороты уже не 0: в коде так и написано — «let's not show a warning if we are just starting to spin». На раскрутке сбой лишь сбрасывает синхронизацию.


19з.4 Датчики: термистор, напряжение и отказ

Температурные датчики — термисторы с отрицательным коэффициентом в делителе с подтягивающим резистором. Цепочка пересчёта собрана из двух функций (controllers/sensors/converters/):

Датчик охлаждающей жидкости по умолчанию (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. Две странности видны сразу:

На 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 Итоги


Дальше: раздел 19и «J1939: CAN грузовиков и спецтехники — PGN, SPN и диагностика DM1» — как та же шина CAN работает на грузовиках и спецтехнике: 29-битные адреса, PGN и коды DM1.

Открыть в интерактивном справочнике