19м. rusEFI изнутри: искра и впрыск по углу коленвала — накопление, опережение и очередь событий на одном таймере
В разделе 19з декодер колеса 60-2 превратил фронты датчика в фазу цикла и обороты. Здесь — что блок делает дальше: как угол «искра за 35° до верхней мёртвой точки» становится моментом времени, когда включить катушку, чтобы она успела набрать ток, когда открыть форсунку, как всё это успевает один аппаратный таймер и что ломается, когда двигатель разгоняется или сбрасывает обороты между зубьями.
Рядом с кодом rusEFI — свой планировщик на C того же устройства и модель коленвала с меняющимися оборотами: на ней сравниваются три способа ставить события. Стенд прогнан на компьютере и тем же исходником в QEMU на Cortex-M4F; прошивка для STM32F407 на TIM5 собрана и измерена, на плате не запускалась.
Опоры: исходники rusEFI — github.com/rusefi/rusefi (коммит 79ceeb32),
лицензия GPL-3.0 с тремя оговорками в license.txt (не для дорог общего
пользования, не по экологическим нормам, не для пилотируемой авиации);
цитаты — по нескольку строк с путём к файлу. Свой код написан заново: он
повторяет устройство rusEFI, а не его текст. Документы: RM0090 (векторы
прерываний и тактирование таймеров STM32F407), DS8626 (альтернативные
функции выводов).
Как это называется в документации
Оригинал Перевод TDC, BTDC, ATDC верхняя мёртвая точка (ВМТ), до ВМТ, после ВМТ engine cycle, engine phase цикл двигателя (720°), фаза — угол в цикле ignition timing, advance момент зажигания, угол опережения (УОЗ) dwell, overdwell накопление (катушка под током перед искрой), перезаряд injection phase, offset; timing mode End / Start / Center фаза впрыска: угол конца / начала / середины импульса injector pulse width длительность импульса форсунки event queue, scheduler, executor очередь событий по времени, планировщик output compare сравнение: прерывание, когда счётчик таймера дошёл до числа late delay, spin wait задержка очереди: близкое событие ждать в цикле angle-based event, trigger scheduler событие по углу: ждёт «своего» зуба rev limiter, hysteresis отсечка, возврат ниже порога
19м.1 От зуба к искре: что делает блок после декодера
Каждый зуб приходит в mainTriggerCallback()
(firmware/controllers/engine_cycle/main_trigger_callback.cpp): время
фронта в тактах 4 МГц, фаза этого зуба (currentPhase) и следующего
(nextPhase). Если оборотов ещё нет (rpm == 0), функция выходит. Иначе —
три шага:
// firmware/controllers/engine_cycle/main_trigger_callback.cpp (rusEFI, GPL-3.0)
handleFuel(edgeTimestamp, currentPhase, nextPhase);
engine->module<TriggerScheduler>()->scheduleEventsUntilNextTriggerTooth(
rpm, edgeTimestamp, currentPhase, nextPhase);
/**
* For spark we schedule both start of coil charge and actual spark based on trigger angle
*/
onTriggerEventSparkLogic(rpm, edgeTimestamp, currentPhase, nextPhase);
Топливо — форсунки, чьё открытие попадает между этим и следующим зубом; затем события, которые ждали этого зуба; затем катушки, чьё накопление начинается до следующего зуба. Главная мысль: на зубе ставится только то, что случится до следующего зуба. От зуба до угла остаётся меньше шага колеса (6° у 60-2, 18° на пропуске), и только этот остаток переводится во время по оборотам. Ошибка оборотов умножается на несколько градусов, а не на весь цикл.
Двигатель стенда: четыре цилиндра, порядок работы 1-3-4-2, ВМТ сжатия — 0, 180, 360 и 540° цикла, катушка и форсунка на каждый цилиндр; фаза цикла считается известной, как с датчиком распредвала.
19м.2 План искры: опережение и накопление
Угол опережения rusEFI берёт из таблицы ignitionTable по оборотам
и нагрузке. Таблицу по умолчанию строит buildTimingMap(35) из
controllers/algo/defaults/default_ignition.cpp: getAdvanceForRpm()
даёт 10° ниже 600 об/мин, 35° с 3000 и прямую между ними,
getInitialAdvance() добавляет 0.1 * (100 - map) — при 100 кПа во
впуске ноль. Эту прямую повторяет advance_deg() стенда.
Накопление — время: по умолчанию setConstantDwell(4), 4 мс на всех
оборотах; угол накопления — время,
делённое на время градуса (ignition_state.cpp). Углы цилиндра считает
prepareCylinderIgnitionSchedule() в spark_logic.cpp:
// firmware/controllers/engine_cycle/spark_logic.cpp (rusEFI, GPL-3.0)
angle_t sparkAngle =
// Negate because timing *before* TDC, and we schedule *after* TDC
- finalIgnitionTiming
// Offset by this cylinder's position in the cycle
+ getPerCylinderFiringOrderOffset(event->cylinderIndex, coilIndex);
и dwellStartAngle = sparkAngle - dwellAngleDuration. У стенда:
/* План искры: угол искры = ВМТ − опережение, накопление — заданное время,
* переведённое в градусы по оборотам на момент плана. */
static void plan_ign(ign_ev *e, float rpm)
{
e->spark_angle = wrap720(e->tdc - advance_deg(rpm));
e->dwell_angle = wrap720(e->spark_angle - C.dwell_ms / ms_per_deg(rpm));
}
| Обороты, об/мин | Опережение | Накопление 4 мс | Впрыск 3 мс | Зуб 6° |
|---|---|---|---|---|
| 600 | 10,0° | 14,40° | 10,80° | 1666,7 мкс |
| 1000 | 14,2° | 24,00° | 18,00° | 1000,0 мкс |
| 2000 | 24,6° | 48,00° | 36,00° | 500,0 мкс |
| 3000 | 35,0° | 72,00° | 54,00° | 333,3 мкс |
| 6000 | 35,0° | 144,00° | 108,00° | 166,7 мкс |
| 7000 | 35,0° | 168,00° | 126,00° | 142,9 мкс |
На 7000 об/мин накопление — 168°, 28 шагов колеса: катушка начинает
заряжаться задолго до искры, мимо проходят десятки зубьев. План
пересчитывается после каждой искры: fireSparkAndPrepareNextSchedule()
гасит катушку и зовёт prepareCylinderIgnitionSchedule() для следующего
цикла; у стенда — fire().
19м.3 Очередь по времени на одном таймере
Все события сходятся в одну очередь: EventQueue (controllers/system/timer/event_queue.cpp)
и SingleTimerExecutor (single_timer_executor.cpp). Это отсортированный
по времени список с линейной вставкой (в комментарии: событий мало),
пул из 64 слотов (m_pool[64]) для событий «выстрелил и забыл» и один
таймер в режиме сравнения, заведённый на голову списка. На STM32 это
TIM5 (port_mpu_util.h: SCHEDULER_TIMER_DEVICE TIM5, 4 МГц); тот же
счётчик — часы всей прошивки (getTimeNowLowerNt() читает TIM5->CNT),
так что время зуба и время события меряются одной линейкой.
У головы очереди два порога. Задержка очереди:
// firmware/controllers/system/timer/single_timer_executor.cpp (rusEFI, GPL-3.0)
// 8us is roughly the cost of the interrupt + overhead of a single timer event
: queue(US2NT(8))
До события меньше 8 мкс — обработчик досиживает до срока в цикле:
// firmware/controllers/system/timer/event_queue.cpp (rusEFI, GPL-3.0)
// near future - spin wait for the event to happen and avoid the
// overhead of rescheduling the timer.
// yes, that's a busy wait but that's what we need here
while (current->getMomentNt() > getTimeNowNt()) {
UNIT_TEST_BUSY_WAIT_CALLBACK();
}
И нижняя граница: setHardwareSchedulerTimer() (microsecond_timer.cpp)
не заводит таймер ближе US2NT(2) — иначе срок окажется в прошлом.
Стенд повторяет обе вещи:
/* Прерывание сравнения: выполнить всё, чей срок наступил или наступит
* в пределах late, и завести таймер на следующее. */
void evq_isr(evq_t *q)
{
q->irq++;
q->in_isr = 1;
for (;;) {
ev_slot *s = q->head;
if (!s)
break;
tick_t now = plat_now();
if (s->at > now + q->late)
break;
if (s->at > now) {
plat_wait_until(s->at); /* дешевле подождать, чем выйти и войти снова */
q->spin++;
}
q->head = s->next;
ev_fn fn = s->fn;
void *arg = s->arg;
release(q, s); /* слот свободен до вызова: действие может поставить его снова */
q->run++;
fn(arg);
}
q->in_isr = 0;
arm_timer(q);
}
arm_timer() заводит сравнение на голову: опоздавшее событие — через
задержку очереди, и не ближе 2 мкс. Слот освобождается до вызова
действия, чтобы действие могло поставить себя снова. Слот, который уже
стоит в очереди, второй раз не ставится — как в insertTask() у rusEFI
(if (scheduling->action) → return false).
Опыт: нужна ли задержка. 6000 об/мин, 1 с. Конец впрыска нарочно поставлен на −394,95°: на 0,05° позже искры соседнего цилиндра, то есть через 1,4 мкс — ближе 2 мкс:
задержка 0 мкс: прерываний таймера 799, событий 799, дождались в цикле 0; длина очереди до 3, слотов пула до 2 из 32; искра от -0.002 до -0.002 град, конец впрыска от 0.020 до 0.020 град
задержка 8 мкс: прерываний таймера 599, событий 799, дождались в цикле 200; длина очереди до 3, слотов пула до 2 из 32; искра от -0.002 до -0.002 град, конец впрыска от -0.007 до -0.007 град
Без задержки каждое событие — своё прерывание, и закрытие форсунки уезжает на 2 мкс после искры: +0,020°. С задержкой 8 мкс 200 закрытий дождались в цикле и пришли вовремя, прерываний 599 вместо 799. Цена — до 8 мкс процессор крутится в обработчике. Очередь не длиннее 3 событий, из пула занято не больше 2 слотов из 32.
19м.4 Угол во время: «ждём своего зуба»
Перевод угла во время — scheduleByAngle() в rpm_calculator.cpp:
delayUs = oneDegreeUs * angle, где oneDegreeUs — время градуса по
оборотам, и schedule() на nowNt плюс сдвиг. Время 64-битное, а сдвиг
sumTickAndFloat() (util/efitime.h) приводит к 32 битам: return ticks
+ (int32_t) extra; — комментарий объясняет, что сдвиг меньше 10 с,
а 32 бит хватает на 500 с тактов. Зачем — в 19м.8.
События по углу — TriggerScheduler (controllers/system/timer/trigger_scheduler.cpp):
// firmware/controllers/system/timer/trigger_scheduler.cpp (rusEFI, GPL-3.0)
if (event->shouldSchedule(currentPhase, nextPhase)) {
// if we're due now, just schedule the event
scheduleByAngle(
&event->eventScheduling,
edgeTimestamp,
event->getAngleFromNow(currentPhase),
action
);
return true;
} else {
// If not due now, add it to the queue to be scheduled later
schedule(msg, event, action);
return false;
}
}
shouldSchedule() — это isPhaseInRange(angle, currentPhase, nextPhase):
попадает ли угол между этим зубом и следующим, с переходом 720 → 0. Если
нет, событие ждёт в списке, и на каждом зубе
scheduleEventsUntilNextTriggerTooth() переводит во время всё, чему пора.
В стенде то же делают phase_in_range(), sched_by_angle(),
ang_schedule_or_queue() и ang_on_tooth() (ang19m.c), сдвиг в тактах —
через int32_t (макрос F2T в plat19m.h). Зажигание на зубе целиком:
/* Как rusEFI: на каждом зубе — только то, что случится до следующего зуба. */
static void schedule_from_tooth(tick_t edge, float cur, float next)
{
ang_on_tooth(&g_q, edge, cur, next); /* искры, ждавшие этого зуба */
for (int i = 0; i < CYL; i++) {
inj_ev *f = &inj[i];
if (!f->open && phase_in_range(f->start_angle, cur, next)) {
if (D.limited)
g_st.cut_fuel++;
else
start_injection(f, edge, angle_from(f->start_angle, cur));
}
}
for (int i = 0; i < CYL; i++) {
ign_ev *e = &ign[i];
if (e->charging || !phase_in_range(e->dwell_angle, cur, next))
continue;
if (D.limited) {
g_st.cut_spark++; /* катушку не заряжаем — искры не будет */
continue;
}
tick_t charge_at = sched_by_angle(&g_q, &e->dwell_slot, edge, angle_from(e->dwell_angle, cur),
coil_on, e);
int by_time = ang_schedule_or_queue(&g_q, &e->spark, edge, e->spark_angle, coil_fire, e, cur, next);
if (!by_time && C.overdwell)
sched(&e->spark.slot, charge_at + F2T(1.5f * C.dwell_ms * 1000.0f * (TICK_HZ / 1000000)),
coil_overdwell, e);
}
}
19м.5 Впрыск: открыть по углу, закрыть по времени
Открытие форсунки ставится по углу, закрытие — через длительность от
открытия (InjectionEvent::onTriggerTooth()):
// firmware/controllers/engine_cycle/main_trigger_callback.cpp (rusEFI, GPL-3.0)
// Schedule opening (stage 1 + stage 2 open together)
efitick_t startTime = scheduleByAngle(nullptr, nowNt, angleFromNow, startAction);
// Schedule closing stage 1
efitick_t turnOffTimeStage1 = startTime + US2NT((int)durationUsStage1);
nullptr — слот из пула. Импульс короче 50 мкс не ставится: события
могут поменяться местами, и форсунка останется открытой. Угол открытия
считает computeInjectionAngle() (fuel_schedule.cpp). Фаза injectionOffset — градусы после ВМТ
сжатия, по умолчанию PORT_INJECTION_OFFSET −400° (defaults.h): впрыск
кончается на такте выпуска, на закрытый впускной клапан. В режиме
InjectionTimingMode::End открытие сдвинуто назад на всю длительность:
// firmware/controllers/engine_cycle/fuel_schedule.cpp (rusEFI, GPL-3.0)
angle_t openingAngle = injectionOffset - injectionDurationAngle;
Стенд — в режиме End, импульс 3 мс:
/* Импульс форсунки: открыть по углу, закрыть по времени. */
static void start_injection(inj_ev *e, tick_t edge, float offset)
{
tick_t open_at = edge + F2T(offset * g_tick_per_deg);
e->planned_end = e->end_angle;
sched(0, open_at, inj_open, e);
sched(0, open_at + F2T(C.inj_ms * 1000.0f * (TICK_HZ / 1000000)), inj_close, e);
}
Угол следующего импульса пересчитывается при закрытии (у rusEFI
turnInjectionPinLow() зовёт event->update(), у стенда inj_close() —
plan_inj()), то есть по оборотам прошлого цикла.
19м.6 Разгон и сброс между зубьями: три способа на модели
Модель коленвала (eng19m.c) задаёт обороты отрезками с постоянным
ускорением, угол и время прихода к углу считаются точно, в double;
из неё же берутся зубья 60-2. Часы виртуальные — время прыгает к
ближайшему зубу или сравнению таймера. Когда ЭБУ гасит катушку, стенд
смотрит, на каком угле вал на самом деле, и вычитает плановый угол.
Плюс — искра позже плана.
Способы: от метки — на первом зубе после пропуска весь цикл вперёд
переводится во время по оборотам за цикл; от зуба — как rusEFI,
обороты средние за прошлые 720°; от зуба, мгновенные — обороты по
последним 90°, как с настройкой rusEFI alwaysInstantRpm:
// firmware/controllers/engine_cycle/rpm_calculator.cpp (rusEFI, GPL-3.0)
if (alwaysInstantRpm) {
rpmState->setRpmValue(instantRpm);
Замер — после трёх циклов, когда ЭБУ уже знает обороты; накопление 4 мс, импульс 3 мс, страж включён.
| Опыт | Способ | Искр | Ошибка искры, ° | Средняя, ° | Накопление, мс | По защите | Отказов | Конец впрыска, ° |
|---|---|---|---|---|---|---|---|---|
| разгон 1000 → 7000 за 1 с | от метки | 134 | 0,110…249,077 | 19,489 | 3,447…4,000 | 0 | 8 | 1,561…298,333 |
| разгон 1000 → 7000 за 1 с | от зуба | 141 | −0,005…2,099 | 0,144 | 2,047…3,997 | 0 | 0 | 1,545…18,985 |
| разгон 1000 → 7000 за 1 с | от зуба, мгновенные | 141 | −0,012…0,144 | 0,009 | 2,589…4,000 | 0 | 0 | −0,020…10,050 |
| рывок 2000 → 6000 за 0,25 с | от метки | 69 | 0,002…161,192 | 20,443 | 4,000…4,000 | 0 | 6 | −0,002…231,043 |
| рывок 2000 → 6000 за 0,25 с | от зуба | 73 | 0,002…2,241 | 0,211 | 2,370…4,000 | 0 | 0 | −0,002…27,320 |
| рывок 2000 → 6000 за 0,25 с | от зуба, мгновенные | 73 | −0,006…0,150 | 0,011 | 2,906…4,000 | 0 | 0 | −0,002…14,486 |
| сброс 6000 → 1500 за 0,5 с | от метки | 72 | −155,405…−0,001 | 24,682 | 4,000…4,000 | 0 | 0 | −158,419…−0,414 |
| сброс 6000 → 1500 за 0,5 с | от зуба | 69 | −1,258…−0,001 | 0,108 | 4,012…6,266 | 3 | 0 | −20,856…−0,387 |
| сброс 6000 → 1500 за 0,5 с | от зуба, мгновенные | 72 | −0,092…0,000 | 0,011 | 4,000…5,586 | 0 | 0 | −10,916…−0,001 |
Ровно 3000 — способы равны: 100 искр, ошибка −0,002° — округление
до такта 0,25 мкс и точность float.
От метки — непригодно при любой смене оборотов. На разгоне ошибка до 249,077° — больше, чем 180° между цилиндрами; слоты прошлого цикла ещё ждут срока, когда приходит новая метка: 8 отказов очереди, искр 134 вместо 141. На сбросе искра раньше плана, ошибка до −155,405°.
От зуба — градусы вместо сотен градусов: на разгоне до 2,099°, на рывке до 2,241°. Ни одна искра не переводилась во время дальше 6° от зуба; остаток ошибки — от оборотов, средние за 720° отстают почти на цикл. Мгновенные обороты убирают и его: до 0,144° на разгоне, 0,150° на рывке, −0,092° на сбросе.
Накопление и впрыск уязвимы при любом способе. Угол накопления посчитан при прошлой искре, на разгоне градусы проходят быстрее — катушка заряжается 2,047 мс вместо 4 (мгновенные — 2,589 мс), на сбросе до 6,266 мс. Конец впрыска на разгоне опаздывает до 18,985° (мгновенные — 10,050°).
19м.7 Страж перезаряда и отсечка
Страж. Искра, которая ждёт зуба, может его не дождаться: датчик
оборвался, двигатель заглох — катушка останется под током. Если искра
легла в список ожидания, scheduleSparkEvent() (spark_logic.cpp)
ставит в тот же слот искру «по часам»:
// firmware/controllers/engine_cycle/spark_logic.cpp (rusEFI, GPL-3.0)
// auto fire spark at 1.5x nominal dwell
efitick_t fireTime = sumTickAndFloat(chargeTime, MSF2NT(1.5f * dwellMs));
Пришёл зуб — scheduleEventsUntilNextTriggerTooth() снимает стража
и ставит искру по углу; не пришёл — overFireSparkAndPrepareNextSchedule()
гасит катушку (в стенде — coil_overdwell()). Опыт: 3000 об/мин, зубья
пропадают, когда катушка 1 начала заряжаться:
защита 1,5 накопления: искр по защите 1, катушка под током до 6.000 мс
без защиты: искр по защите 0, катушка под током до 4.000 мс, к концу опыта не погасли катушек 1, горит уже 115.9 мс
Со стражем катушка погасла через 6 мс (1,5 × 4). Без него горит до конца опыта — 115,9 мс — и дальше, пока не вернутся зубья. Обратная сторона: на сбросе 6000 → 1500 способом «от зуба» страж сработал 3 раза — угол накопления посчитан по ещё высоким оборотам, вал замедлился, 6 мс истекли раньше зуба искры, и искра проскочила раньше плана на 9,337°. С мгновенными оборотами — ни разу.
Отсечка. LimpManager (controllers/limp_manager.cpp) разрешает или
запрещает топливо и искру. По умолчанию (default_base_engine.cpp)
rpmHardLimit = 7000, rpmHardLimitHyst = 50, режутся оба:
// firmware/controllers/limp_manager.cpp (rusEFI, GPL-3.0)
updateRevLimit(rpm);
if (m_revLimitHysteresis.test(rpm, m_revLimit, resumeRpm)) {
if (engineConfiguration->cutFuelOnHardLimit) {
allowFuel.clear(ClearReason::HardLimit);
}
if (engineConfiguration->cutSparkOnHardLimit) {
allowSpark.clear(ClearReason::HardLimit);
}
Hysteresis::test() (hysteresis.h) ставит запрет выше 7000 и снимает
ниже 6950; обновляет его onFastCallback() — быстрый цикл
(FAST_CALLBACK_RATE — Period200hz в main_loop.h), а не зуб. При
запрете накопление не начинается, но начатое доводится до искры, чтобы
катушка не сгорела. Стенд проверяет порог раз за цикл:
if (cur == 0.0f) { /* начало цикла: обороты за цикл и ограничитель */
if (D.have_cycle) {
D.rpm_cycle = 120.0f * (float)TICK_HZ / T2F(edge - D.cycle_t);
if (D.rpm_cycle > g_st.rpm_max)
g_st.rpm_max = D.rpm_cycle;
if (D.rpm_cycle > C.rev_limit)
D.limited = 1;
else if (D.rpm_cycle < C.rev_limit - C.rev_hyst)
D.limited = 0;
}
D.cycle_t = edge;
D.have_cycle = 1;
}
Игрушечная модель: с искрами обороты растут на 4000 об/мин за секунду, без искр падают на 8000; старт с 6500, 1 с:
срезано искр 72, впрысков 72; оборотов без искр 42; обороты ЭБУ за цикл до 7087.4; настоящие обороты от 6717.9 до 7100.0
Возврат — 50 об/мин, а вал ходит от 6717,9 до 7100,0: решение принимается раз за цикл по средним за прошлый цикл оборотам.
19м.8 Прошивка для STM32F407 и тот же стенд в QEMU
fw19m.c: время и очередь, как у rusEFI, на одном 32-битном TIM5; зубья
в этой прошивке ловит захват на том же таймере. 168 МГц от кварца 8 МГц,
APB1 — 42 МГц, таймеры на ней — удвоенные 84 МГц (RM0090), делитель 21 →
4 МГц. Канал 2 (PA1, AF2 по DS8626) — захват зубьев, канал 1 — сравнение.
Вектор TIM5 — номер 50 (RM0090; TIM5_IRQn = 50 в stm32f407xx.h).
Режим сравнения OC1M = 001, как у rusEFI
(microsecond_timer_stm32.cpp: STM32_TIM_CCMR1_OC1M(1)):
void plat_set_compare(tick_t t)
{
TIM5->CCR1 = (uint32_t)t;
TIM5->SR = ~TIM_SR_CC1IF;
TIM5->DIER |= TIM_DIER_CC1IE;
if ((int32_t)((uint32_t)t - TIM5->CNT) <= 0)
TIM5->EGR = TIM_EGR_CC1G; /* срок прошёл, пока писали регистр, — прерывание сразу */
}
Срок прошёл, пока писали CCR1, — совпадения не будет до следующего
оборота счётчика (1073 с), поэтому событие запрашивается программно
(EGR = CC1G); у rusEFI такая ветка есть для AT32F4.
Сборка arm-none-eabi-gcc 12.2.1, -Os, Cortex-M4F:
text data bss dec hex filename
542 0 0 542 21e evq19m.o
446 0 8 454 1c6 ang19m.o
2114 0 2072 4186 105a ecu19m.o
558 0 8 566 236 fw19m.o
374 0 0 374 176 start19m.o
| Сборка | Код (text) | ОЗУ (bss) | Библиотечные функции |
|---|---|---|---|
fw19m.elf, сдвиг через int32_t |
4592 | 2088 | нет |
fw64.elf, -DTICK_CAST64 |
6824 | 2088 | __aeabi_dadd, __aeabi_dmul, __aeabi_f2lz, __aeabi_l2f |
В evq19m, ang19m и ecu19m нет вызовов библиотеки double, в
ecu19m.o 140 команд FPU. Прямое float → int64_t
(-DTICK_CAST64) приводит __aeabi_f2lz, __aeabi_l2f, а с ними
__aeabi_dadd и __aeabi_dmul: +2232 байта и программная
арифметика double в обработчике зуба. Вот зачем sumTickAndFloat().
Тот же sim19m.c собран под Cortex-M4F (-O2) и запущен в QEMU 7.2.22
на netduinoplus2 (STM32F405, то же ядро) с выводом через полухостинг.
Часы стенда виртуальные, и вывод совпал с компьютером знак в знак во
всех пяти опытах: float на FPU считает план и перевод угла так же.
Захват и сравнение TIM5 в этом прогоне не участвуют.
19м.9 Грабли
Искра уезжает на десятки градусов при разгоне. Весь цикл поставлен от метки — ставить только до следующего зуба.
На разгоне слабая искра. Угол накопления посчитан по старым оборотам (2,047 мс вместо 4); мгновенные обороты помогают не до конца.
На сбросе газа искра раньше на 9,337°. Сработал страж перезаряда. Лечится точными оборотами, а не отключением стража.
Событие через 1–2 мкс после соседнего опаздывает. Таймер не заводится ближе 2 мкс — близкие события ждать в цикле (задержка очереди).
Событие молча не встаёт в очередь. Его слот ещё ждёт прошлого срока;
у rusEFI insertTask() возвращает false.
float → int64 тянет double. Сдвиг меньше 536 с помещается
в int32_t тактов 4 МГц — приводить к нему.
Отсечка «пилит» сильнее гистерезиса. Решение раз за цикл по средним оборотам.
Сравнение пропущено, пока писали регистр. Сверить срок со счётчиком
и при опоздании выставить CC1G.
19м.10 Итоги
- rusEFI на каждом зубе ставит только события до следующего зуба; во время переводится остаток меньше шага колеса.
- Опережение по умолчанию — от 10° на 600 об/мин до 35° с 3000, накопление 4 мс; на 7000 об/мин это 168° цикла.
- Одна очередь на одном 32-битном TIM5, 4 МГц; события ближе 8 мкс ждутся в цикле, таймер не ближе 2 мкс. На 6000 об/мин — 799 событий за 599 прерываний, очередь не длиннее 3.
- Разгон 1000 → 7000 об/мин за 1 с, ошибка искры: от метки — до 249,077°, от зуба — до 2,099°, по мгновенным оборотам — до 0,144°.
- Накопление на разгоне — 2,047 мс, конец впрыска позже на 18,985°; с мгновенными оборотами — 2,589 мс и 10,050°.
- Страж 1,5 × накопления гасит катушку через 6 мс при обрыве датчика; на резком сбросе он же даёт 3 ранние искры (до 9,337°).
- Отсечка 7000/6950 раз за цикл: обороты 6717,9…7100,0.
- Прошивка F407: 4592 байта кода, 2088 байт ОЗУ, без
double; с прямымfloat→int64_t— 6824 байта. Стенд в QEMU совпал с компьютером знак в знак.
Полные исходники
Каталог _verify/rusefi19m/; копии цитируемых файлов rusEFI с license.txt — в rusefi/.
plat19m.h — что очередь и ЭБУ ждут от платформы: время, таймер, выходы.
/* plat19m.h — раздел 19м: что планировщик и ЭБУ ждут от платформы.
* Время — такты 4 МГц (0,25 мкс), как у rusEFI; 64 бита, чтобы не думать о переполнении.
* На компьютере и в QEMU эти функции ведут виртуальные часы стенда (sim19m.c),
* на STM32F407 — таймер TIM5 (fw19m.c). */
#ifndef PLAT19M_H
#define PLAT19M_H
#include <stdint.h>
typedef int64_t tick_t;
#define TICK_HZ 4000000
#define US2T(us) ((tick_t)(us) * (TICK_HZ / 1000000))
/* float <-> такты через int32, как sumTickAndFloat() в rusEFI: преобразование float <-> int64
* на Cortex-M4 делает библиотека, и идёт она через double. Сдвиги здесь меньше 536 с. */
#ifdef TICK_CAST64
#define F2T(x) ((tick_t)(x))
#define T2F(x) ((float)(x))
#else
#define F2T(x) ((tick_t)(int32_t)(x))
#define T2F(x) ((float)(int32_t)(x))
#endif
tick_t plat_now(void); /* текущее время, такты */
void plat_wait_until(tick_t t); /* короткое ожидание в цикле (не дольше задержки очереди) */
void plat_set_compare(tick_t t); /* завести прерывание таймера на момент t */
void plat_pin(int ch, int level); /* выход: 0..3 — катушки, 4..7 — форсунки */
#endif
evq19m.h — очередь событий по времени.
/* evq19m.h — раздел 19м: очередь событий по времени на одном аппаратном таймере.
* Устроена как EventQueue + SingleTimerExecutor в rusEFI: отсортированный список,
* пул свободных слотов, одно прерывание сравнения на всю очередь. */
#ifndef EVQ19M_H
#define EVQ19M_H
#include "plat19m.h"
typedef void (*ev_fn)(void *arg);
typedef struct ev_slot {
struct ev_slot *next;
tick_t at; /* когда выполнить */
ev_fn fn; /* NULL — слот не стоит в очереди */
void *arg;
uint8_t pooled; /* слот из пула: после выполнения вернётся в пул */
} ev_slot;
#define EVQ_POOL 32
typedef struct {
ev_slot *head; /* очередь по возрастанию at */
ev_slot *free; /* свободные слоты пула */
ev_slot pool[EVQ_POOL];
tick_t late; /* событие ближе этого срока ждём в цикле, а не заводим таймер */
uint8_t in_isr;
/* счётчики для стенда */
uint32_t irq; /* прерываний таймера */
uint32_t run; /* выполнено событий */
uint32_t spin; /* из них дождались в цикле */
uint32_t refused; /* отказов: слот уже стоит в очереди или пул пуст */
uint32_t max_len; /* самая длинная очередь */
uint32_t used, max_pool;
} evq_t;
void evq_init(evq_t *q, tick_t late);
int evq_schedule(evq_t *q, ev_slot *s, tick_t at, ev_fn fn, void *arg);
void evq_cancel(evq_t *q, ev_slot *s);
void evq_isr(evq_t *q);
int evq_len(const evq_t *q);
#endif
evq19m.c — очередь событий: вставка, отмена, прерывание сравнения.
/* evq19m.c — раздел 19м: очередь событий по времени на одном таймере сравнения. */
#include "evq19m.h"
void evq_init(evq_t *q, tick_t late)
{
q->head = 0;
q->free = 0;
q->late = late;
q->in_isr = 0;
q->irq = q->run = q->spin = q->refused = q->max_len = q->used = q->max_pool = 0;
for (int i = 0; i < EVQ_POOL; i++) {
q->pool[i].fn = 0;
q->pool[i].pooled = 1;
q->pool[i].next = q->free;
q->free = &q->pool[i];
}
}
int evq_len(const evq_t *q)
{
int n = 0;
for (const ev_slot *s = q->head; s; s = s->next)
n++;
return n;
}
/* Вставка по времени — линейный поиск, событий в очереди единицы.
* Возвращает 1, если событие встало в голову: тогда таймер надо перевести. */
static int insert(evq_t *q, ev_slot *s)
{
if (!q->head || s->at < q->head->at) {
s->next = q->head;
q->head = s;
return 1;
}
ev_slot *p = q->head;
while (p->next && p->next->at <= s->at)
p = p->next;
s->next = p->next;
p->next = s;
return 0;
}
static void release(evq_t *q, ev_slot *s)
{
s->fn = 0;
s->next = 0;
if (s->pooled) {
s->next = q->free;
q->free = s;
q->used--;
}
}
/* Завести таймер на голову очереди. Опоздавшее событие — как можно скорее,
* но не ближе 2 мкс: иначе сравнение может проскочить мимо счётчика. */
static void arm_timer(evq_t *q)
{
if (!q->head)
return;
tick_t now = plat_now(), at = q->head->at;
if (at <= now)
at = now + q->late;
if (at < now + US2T(2))
at = now + US2T(2);
plat_set_compare(at);
}
/* s == NULL — взять слот из пула (событие «выстрелил и забыл»).
* Возвращает 1 — встало в голову, 0 — не в голову, -1 — отказ. */
int evq_schedule(evq_t *q, ev_slot *s, tick_t at, ev_fn fn, void *arg)
{
if (!s) {
s = q->free;
if (!s) {
q->refused++;
return -1;
}
q->free = s->next;
if (++q->used > q->max_pool)
q->max_pool = q->used;
} else if (s->fn) {
q->refused++; /* слот уже ждёт своего срока: rusEFI тоже отказывает */
return -1;
}
s->at = at;
s->fn = fn;
s->arg = arg;
int first = insert(q, s);
uint32_t n = (uint32_t)evq_len(q);
if (n > q->max_len)
q->max_len = n;
if (first && !q->in_isr)
arm_timer(q);
return first;
}
void evq_cancel(evq_t *q, ev_slot *s)
{
if (!s->fn)
return;
for (ev_slot **pp = &q->head; *pp; pp = &(*pp)->next)
if (*pp == s) {
*pp = s->next;
break;
}
release(q, s);
}
/* Прерывание сравнения: выполнить всё, чей срок наступил или наступит
* в пределах late, и завести таймер на следующее. */
void evq_isr(evq_t *q)
{
q->irq++;
q->in_isr = 1;
for (;;) {
ev_slot *s = q->head;
if (!s)
break;
tick_t now = plat_now();
if (s->at > now + q->late)
break;
if (s->at > now) {
plat_wait_until(s->at); /* дешевле подождать, чем выйти и войти снова */
q->spin++;
}
q->head = s->next;
ev_fn fn = s->fn;
void *arg = s->arg;
release(q, s); /* слот свободен до вызова: действие может поставить его снова */
q->run++;
fn(arg);
}
q->in_isr = 0;
arm_timer(q);
}
ang19m.h — события по углу коленвала.
/* ang19m.h — раздел 19м: события по углу коленвала поверх очереди по времени.
* Как TriggerScheduler в rusEFI: событие ждёт «своего» зуба, а от зуба до угла
* остаётся меньше шага колеса — этот остаток и переводится во время. */
#ifndef ANG19M_H
#define ANG19M_H
#include "evq19m.h"
#define ENGINE_CYCLE 720.0f
typedef struct ang_ev {
struct ang_ev *next; /* список «ждём зуба» */
float angle; /* угол цикла 0…720 */
float offset; /* сколько градусов от зуба пришлось перевести во время */
ev_slot slot; /* слот в очереди по времени */
ev_fn fn;
void *arg;
uint8_t waiting;
} ang_ev;
extern float g_tick_per_deg; /* тактов таймера на градус при текущих оборотах */
int phase_in_range(float test, float cur, float next);
float angle_from(float target, float cur);
tick_t sched_by_angle(evq_t *q, ev_slot *s, tick_t edge, float angle, ev_fn fn, void *arg);
int ang_schedule_or_queue(evq_t *q, ang_ev *e, tick_t edge, float angle, ev_fn fn, void *arg,
float cur, float next);
void ang_on_tooth(evq_t *q, tick_t edge, float cur, float next);
void ang_cancel(ang_ev *e);
void ang_reset(void);
#endif
ang19m.c — события по углу: «ждём своего зуба» и перевод остатка во время.
/* ang19m.c — раздел 19м: события по углу коленвала. */
#include "ang19m.h"
float g_tick_per_deg;
static ang_ev *waiting_head;
void ang_reset(void)
{
waiting_head = 0;
g_tick_per_deg = 0;
}
/* Попадает ли угол test в промежуток [cur, next) с переходом через 720 -> 0. */
int phase_in_range(float test, float cur, float next)
{
if (next > cur)
return test >= cur && test < next;
return test >= cur || test < next;
}
/* Сколько градусов от cur до target вперёд по циклу. */
float angle_from(float target, float cur)
{
float a = target - cur;
if (a < 0)
a += ENGINE_CYCLE;
return a;
}
/* Угол во время: «через angle градусов от фронта edge» при текущих оборотах. */
tick_t sched_by_angle(evq_t *q, ev_slot *s, tick_t edge, float angle, ev_fn fn, void *arg)
{
tick_t at = edge + F2T(angle * g_tick_per_deg);
evq_schedule(q, s, at, fn, arg);
return at;
}
/* Событие в промежутке до следующего зуба — сразу во время.
* Иначе — в список ожидания до своего зуба. Возвращает 1, если поставлено по времени. */
int ang_schedule_or_queue(evq_t *q, ang_ev *e, tick_t edge, float angle, ev_fn fn, void *arg,
float cur, float next)
{
e->angle = angle;
e->fn = fn;
e->arg = arg;
if (phase_in_range(angle, cur, next)) {
e->offset = angle_from(angle, cur);
sched_by_angle(q, &e->slot, edge, e->offset, fn, arg);
return 1;
}
if (!e->waiting) {
e->waiting = 1;
e->next = waiting_head;
waiting_head = e;
}
return 0;
}
void ang_cancel(ang_ev *e)
{
for (ang_ev **pp = &waiting_head; *pp; pp = &(*pp)->next)
if (*pp == e) {
*pp = e->next;
break;
}
e->waiting = 0;
e->next = 0;
}
/* На каждом зубе: всё, что ждало и попадает в [cur, next), — в очередь по времени.
* Слот события мог быть занят защитой от перезаряда — снимаем её и ставим искру. */
void ang_on_tooth(evq_t *q, tick_t edge, float cur, float next)
{
ang_ev **pp = &waiting_head;
while (*pp) {
ang_ev *e = *pp;
if (phase_in_range(e->angle, cur, next)) {
*pp = e->next;
e->waiting = 0;
e->next = 0;
evq_cancel(q, &e->slot);
e->offset = angle_from(e->angle, cur);
sched_by_angle(q, &e->slot, edge, e->offset, e->fn, e->arg);
} else {
pp = &e->next;
}
}
}
ecu19m.h — ЭБУ: настройки, события искры и впрыска, счётчики.
/* ecu19m.h — раздел 19м: искра и впрыск по углу коленвала, колесо 60-2, 4 цилиндра.
* Порядок работы 1-3-4-2, ВМТ сжатия цилиндров — 0, 180, 360 и 540° цикла;
* фаза цикла (какой из двух оборотов) считается известной, как с датчиком распредвала. */
#ifndef ECU19M_H
#define ECU19M_H
#include "ang19m.h"
#define CYL 4
enum {
ECU_FROM_SYNC = 0, /* всё за цикл вперёд от метки колеса, по оборотам за цикл */
ECU_FROM_TOOTH = 1, /* от ближайшего зуба, по оборотам за цикл — как rusEFI */
ECU_INSTANT = 2 /* от ближайшего зуба, по мгновенным оборотам за 90° */
};
typedef struct {
int mode;
int overdwell; /* 1 — защита от перезаряда: искра не позже 1,5 накопления */
float dwell_ms; /* накопление катушки */
float inj_end; /* конец впрыска, градусы после ВМТ сжатия (−400 — на выпуске) */
float inj_ms; /* длительность импульса форсунки */
float rev_limit; /* отсечка, об/мин */
float rev_hyst; /* возврат ниже отсечки на столько */
int late_us; /* задержка очереди: ближе — ждать в цикле */
} ecu_cfg;
typedef struct {
int cyl;
float tdc; /* ВМТ сжатия этого цилиндра в цикле */
float spark_angle; /* план: угол искры */
float dwell_angle; /* план: угол начала накопления */
ev_slot dwell_slot;
ang_ev spark; /* искра; её слот занимает и защита от перезаряда */
int charging;
tick_t charge_t;
} ign_ev;
typedef struct {
int cyl;
float tdc;
float start_angle, end_angle; /* план: открыть и закрыть */
float planned_end; /* с каким концом поставлен текущий импульс */
int open;
} inj_ev;
typedef struct {
uint32_t teeth, syncs, sync_err;
uint32_t sparks, overdwell, cut_spark, cut_fuel, refused, injections;
float rpm_max;
} ecu_stat;
extern evq_t g_q;
extern ecu_stat g_st;
extern void (*ecu_on_spark)(const ign_ev *e, int by_overdwell);
extern void (*ecu_on_inj)(const inj_ev *e);
void ecu_init(const ecu_cfg *c);
void ecu_tooth(tick_t edge);
float ecu_rpm(void);
float advance_deg(float rpm);
#endif
ecu19m.c — ЭБУ: декодер, обороты, план, искра, впрыск, отсечка.
/* ecu19m.c — раздел 19м: от зуба колеса к искре и впрыску. */
#include <math.h>
#include "ecu19m.h"
#define TEETH 58 /* 60-2: 58 зубьев */
#define TOOTH_DEG 6.0f /* шаг колеса */
#define HIST 20 /* история зубьев для мгновенных оборотов */
evq_t g_q;
ecu_stat g_st;
void (*ecu_on_spark)(const ign_ev *e, int by_overdwell);
void (*ecu_on_inj)(const inj_ev *e);
static ecu_cfg C;
static ign_ev ign[CYL];
static inj_ev inj[CYL];
typedef struct {
tick_t last, prev_dt, cycle_t;
int synced, idx, rev, have_cycle, planned, limited;
float rpm_cycle, rpm_inst;
int32_t cum; /* угол от синхронизации, градусы */
tick_t hist_t[HIST];
int32_t hist_a[HIST];
int hn;
} dec_t;
static dec_t D;
static float wrap720(float a)
{
while (a < 0)
a += ENGINE_CYCLE;
while (a >= ENGINE_CYCLE)
a -= ENGINE_CYCLE;
return a;
}
/* Миллисекунд на градус — как getOneDegreeTimeMs() в rusEFI. */
static float ms_per_deg(float rpm)
{
return 1000.0f * 60 / 360 / rpm;
}
/* Опережение по оборотам: эвристика карты rusEFI по умолчанию (getInitialAdvance)
* при давлении во впуске 100 кПа и наибольшем угле 35°. */
float advance_deg(float rpm)
{
float a;
if (rpm >= 3000.0f)
a = 35.0f;
else if (rpm < 600.0f)
a = 10.0f;
else
a = 10.0f + (rpm - 600.0f) * (35.0f - 10.0f) / (3000.0f - 600.0f);
return roundf(a * 10.0f) / 10.0f;
}
float ecu_rpm(void)
{
return C.mode == ECU_INSTANT ? D.rpm_inst : D.rpm_cycle;
}
/* План искры: угол искры = ВМТ − опережение, накопление — заданное время,
* переведённое в градусы по оборотам на момент плана. */
static void plan_ign(ign_ev *e, float rpm)
{
e->spark_angle = wrap720(e->tdc - advance_deg(rpm));
e->dwell_angle = wrap720(e->spark_angle - C.dwell_ms / ms_per_deg(rpm));
}
/* План впрыска: закрыть на inj_end после ВМТ, открыть раньше на длительность. */
static void plan_inj(inj_ev *e, float rpm)
{
e->end_angle = wrap720(C.inj_end + e->tdc);
e->start_angle = wrap720(e->end_angle - C.inj_ms / ms_per_deg(rpm));
}
static void fire(ign_ev *e, int by_overdwell)
{
plat_pin(e->cyl, 0);
if (e->charging) {
g_st.sparks++;
if (ecu_on_spark)
ecu_on_spark(e, by_overdwell);
}
e->charging = 0;
float rpm = ecu_rpm();
if (rpm > 0)
plan_ign(e, rpm); /* следующий цикл — по свежим оборотам */
}
static void coil_on(void *arg)
{
ign_ev *e = arg;
e->charging = 1;
e->charge_t = plat_now();
plat_pin(e->cyl, 1);
}
static void coil_fire(void *arg)
{
fire(arg, 0);
}
/* Зуб искры не пришёл вовремя: искра по времени, чтобы не сжечь катушку. */
static void coil_overdwell(void *arg)
{
ign_ev *e = arg;
ang_cancel(&e->spark);
g_st.overdwell++;
fire(e, 1);
}
static void inj_open(void *arg)
{
inj_ev *e = arg;
e->open = 1;
plat_pin(4 + e->cyl, 1);
}
static void inj_close(void *arg)
{
inj_ev *e = arg;
e->open = 0;
plat_pin(4 + e->cyl, 0);
g_st.injections++;
if (ecu_on_inj)
ecu_on_inj(e);
float rpm = ecu_rpm();
if (rpm > 0)
plan_inj(e, rpm);
}
static void sched(ev_slot *s, tick_t at, ev_fn fn, void *arg)
{
if (evq_schedule(&g_q, s, at, fn, arg) < 0)
g_st.refused++;
}
/* Импульс форсунки: открыть по углу, закрыть по времени. */
static void start_injection(inj_ev *e, tick_t edge, float offset)
{
tick_t open_at = edge + F2T(offset * g_tick_per_deg);
e->planned_end = e->end_angle;
sched(0, open_at, inj_open, e);
sched(0, open_at + F2T(C.inj_ms * 1000.0f * (TICK_HZ / 1000000)), inj_close, e);
}
/* Старый способ: на метке колеса весь цикл вперёд переводится во время. */
static void schedule_from_sync(tick_t edge)
{
for (int i = 0; i < CYL; i++) {
ign_ev *e = &ign[i];
inj_ev *f = &inj[i];
if (D.limited) {
g_st.cut_spark++;
g_st.cut_fuel++;
continue;
}
e->spark.angle = e->spark_angle;
e->spark.offset = e->spark_angle;
tick_t spark_at = edge + F2T(e->spark_angle * g_tick_per_deg);
tick_t dwell_at = spark_at - F2T(C.dwell_ms * 1000.0f * (TICK_HZ / 1000000));
if (dwell_at < edge)
dwell_at = edge; /* накопление переходит через метку — укорачиваем */
sched(&e->dwell_slot, dwell_at, coil_on, e);
sched(&e->spark.slot, spark_at, coil_fire, e);
start_injection(f, edge, f->start_angle);
}
}
/* Как rusEFI: на каждом зубе — только то, что случится до следующего зуба. */
static void schedule_from_tooth(tick_t edge, float cur, float next)
{
ang_on_tooth(&g_q, edge, cur, next); /* искры, ждавшие этого зуба */
for (int i = 0; i < CYL; i++) {
inj_ev *f = &inj[i];
if (!f->open && phase_in_range(f->start_angle, cur, next)) {
if (D.limited)
g_st.cut_fuel++;
else
start_injection(f, edge, angle_from(f->start_angle, cur));
}
}
for (int i = 0; i < CYL; i++) {
ign_ev *e = &ign[i];
if (e->charging || !phase_in_range(e->dwell_angle, cur, next))
continue;
if (D.limited) {
g_st.cut_spark++; /* катушку не заряжаем — искры не будет */
continue;
}
tick_t charge_at = sched_by_angle(&g_q, &e->dwell_slot, edge, angle_from(e->dwell_angle, cur),
coil_on, e);
int by_time = ang_schedule_or_queue(&g_q, &e->spark, edge, e->spark_angle, coil_fire, e, cur, next);
if (!by_time && C.overdwell)
sched(&e->spark.slot, charge_at + F2T(1.5f * C.dwell_ms * 1000.0f * (TICK_HZ / 1000000)),
coil_overdwell, e);
}
}
void ecu_init(const ecu_cfg *c)
{
C = *c;
D = (dec_t){0};
g_st = (ecu_stat){0};
evq_init(&g_q, US2T(c->late_us));
ang_reset();
for (int i = 0; i < CYL; i++) {
ign[i] = (ign_ev){0};
inj[i] = (inj_ev){0};
ign[i].cyl = inj[i].cyl = i;
ign[i].tdc = inj[i].tdc = 180.0f * (float)i;
}
}
void ecu_tooth(tick_t edge)
{
g_st.teeth++;
tick_t dt = edge - D.last;
D.last = edge;
/* пропуск: зуб в 2,25…3,75 раза длиннее прошлого — окно rusEFI для 60-2 */
int gap = D.prev_dt > 0 && dt * 4 > D.prev_dt * 9 && dt * 4 < D.prev_dt * 15;
D.prev_dt = dt;
if (gap) {
if (D.synced && D.idx != TEETH - 1) {
g_st.sync_err++;
D.synced = 0;
}
if (!D.synced) {
D.synced = 1;
D.rev = 0;
D.cum = 0;
D.hn = 0;
D.have_cycle = 0;
} else {
D.rev ^= 1;
D.cum += 18;
}
D.idx = 0;
g_st.syncs++;
} else if (D.synced) {
D.cum += 6;
if (++D.idx >= TEETH) {
D.synced = 0;
g_st.sync_err++;
}
}
if (!D.synced)
return;
/* мгновенные обороты — по последним 90° (как instant_rpm_calculator.cpp) */
D.hist_t[D.hn % HIST] = edge;
D.hist_a[D.hn % HIST] = D.cum;
D.hn++;
for (int k = 1; k < HIST && k < D.hn; k++) {
int j = (D.hn - 1 - k) % HIST;
if (D.hist_a[j] <= D.cum - 90) {
D.rpm_inst = (float)(D.cum - D.hist_a[j]) * ((float)TICK_HZ / 6.0f) / T2F(edge - D.hist_t[j]);
break;
}
}
float cur = 360.0f * (float)D.rev + TOOTH_DEG * (float)D.idx;
float next = (D.idx == TEETH - 1) ? (D.rev ? 0.0f : 360.0f) : cur + TOOTH_DEG;
if (cur == 0.0f) { /* начало цикла: обороты за цикл и ограничитель */
if (D.have_cycle) {
D.rpm_cycle = 120.0f * (float)TICK_HZ / T2F(edge - D.cycle_t);
if (D.rpm_cycle > g_st.rpm_max)
g_st.rpm_max = D.rpm_cycle;
if (D.rpm_cycle > C.rev_limit)
D.limited = 1;
else if (D.rpm_cycle < C.rev_limit - C.rev_hyst)
D.limited = 0;
}
D.cycle_t = edge;
D.have_cycle = 1;
}
float rpm = ecu_rpm();
if (rpm <= 0)
return; /* оборотов ещё нет — как rusEFI при rpm == 0 */
g_tick_per_deg = (float)TICK_HZ * 60 / 360 / rpm;
if (!D.planned) {
for (int i = 0; i < CYL; i++) {
plan_ign(&ign[i], rpm);
plan_inj(&inj[i], rpm);
}
D.planned = 1;
}
if (C.mode == ECU_FROM_SYNC) {
if (cur == 0.0f)
schedule_from_sync(edge);
} else {
schedule_from_tooth(edge, cur, next);
}
}
eng19m.h — модель коленвала.
/* eng19m.h — раздел 19м: модель коленвала для стенда.
* Обороты меняются по отрезкам с постоянным угловым ускорением; угол — точная парабола,
* время прихода к углу — корень квадратного уравнения. Считается в double. */
#ifndef ENG19M_H
#define ENG19M_H
#define ENG_SEGS 64
typedef struct {
double t0; /* начало отрезка, с */
double th0; /* угол в начале, градусы (накопленный, без деления на 720) */
double w0; /* скорость в начале, °/с */
double a; /* ускорение, °/с² */
} eng_seg;
typedef struct {
eng_seg s[ENG_SEGS];
int n;
} eng_t;
void eng_init(eng_t *m, double th0, double rpm);
void eng_ramp(eng_t *m, double t_from, double dur, double rpm_to);
void eng_accel(eng_t *m, double t_from, double rpm_per_s);
double eng_theta(const eng_t *m, double t);
double eng_rpm(const eng_t *m, double t);
double eng_time_of(const eng_t *m, double th);
#endif
eng19m.c — модель коленвала: отрезки с постоянным ускорением.
/* eng19m.c — раздел 19м: модель коленвала с меняющимися оборотами. */
#include <math.h>
#include "eng19m.h"
#define RPM2W 6.0 /* об/мин -> °/с */
static int seg_at(const eng_t *m, double t)
{
int i = m->n - 1;
while (i > 0 && m->s[i].t0 > t)
i--;
return i;
}
static void state(const eng_seg *s, double t, double *th, double *w)
{
double dt = t - s->t0;
*th = s->th0 + s->w0 * dt + 0.5 * s->a * dt * dt;
*w = s->w0 + s->a * dt;
}
/* Обрезать будущее после t и начать с t новый отрезок с ускорением a. */
static void cut(eng_t *m, double t, double a)
{
int i = seg_at(m, t);
double th, w;
state(&m->s[i], t, &th, &w);
if (i + 1 >= ENG_SEGS - 1) { /* прошлое стенду не нужно: оставляем текущий отрезок */
m->s[0] = m->s[i];
i = 0;
}
if (m->s[i].t0 < t)
i++;
m->s[i] = (eng_seg){t, th, w, a};
m->n = i + 1;
}
static void append_flat(eng_t *m, double t)
{
double th, w;
state(&m->s[m->n - 1], t, &th, &w);
if (w < 0)
w = 0;
if (m->n < ENG_SEGS)
m->s[m->n++] = (eng_seg){t, th, w, 0};
}
void eng_init(eng_t *m, double th0, double rpm)
{
m->n = 1;
m->s[0] = (eng_seg){0, th0, rpm * RPM2W, 0};
}
/* С момента t_from обороты линейно идут к rpm_to за dur секунд, дальше ровно. */
void eng_ramp(eng_t *m, double t_from, double dur, double rpm_to)
{
double th, w;
state(&m->s[seg_at(m, t_from)], t_from, &th, &w);
cut(m, t_from, (rpm_to * RPM2W - w) / dur);
append_flat(m, t_from + dur);
}
/* С момента t_from — постоянное ускорение; если оборотам суждено дойти до нуля — остановка. */
void eng_accel(eng_t *m, double t_from, double rpm_per_s)
{
double a = rpm_per_s * RPM2W;
cut(m, t_from, a);
double w0 = m->s[m->n - 1].w0;
if (a < 0)
append_flat(m, t_from + w0 / -a);
}
double eng_theta(const eng_t *m, double t)
{
double th, w;
state(&m->s[seg_at(m, t)], t, &th, &w);
return th;
}
double eng_rpm(const eng_t *m, double t)
{
double th, w;
state(&m->s[seg_at(m, t)], t, &th, &w);
return w / RPM2W;
}
/* Когда вал дойдёт до угла th; -1 — не дойдёт никогда (встал). */
double eng_time_of(const eng_t *m, double th)
{
for (int i = 0; i < m->n; i++) {
const eng_seg *s = &m->s[i];
if (i + 1 < m->n && th > m->s[i + 1].th0)
continue;
double d = th - s->th0;
if (d <= 0)
return s->t0;
if (s->a == 0)
return s->w0 > 0 ? s->t0 + d / s->w0 : -1;
double disc = s->w0 * s->w0 + 2 * s->a * d;
if (disc < 0)
return -1;
return s->t0 + 2 * d / (s->w0 + sqrt(disc));
}
return -1;
}
sim19m.c — стенд: виртуальные часы, опыты и замеры.
/* sim19m.c — раздел 19м: стенд. Модель вала даёт зубья колеса 60-2, ЭБУ ставит искру
* и впрыск, стенд по модели смотрит, на каком угле вала они случились на самом деле.
* Часы виртуальные: время прыгает к ближайшему событию — зубу или сравнению таймера.
* Один и тот же файл собирается для компьютера (host19m.c) и для Cortex-M4F в QEMU (qemu19m.c). */
#include <math.h>
#include <stdint.h>
#include "ecu19m.h"
#include "eng19m.h"
void sim_out(const char *s);
#define NEVER INT64_MAX
#define TEETH_PER_REV 58
/* ── виртуальные часы и выходы (платформа для evq19m/ecu19m) ─────────────── */
static tick_t now_t, cmp_t;
static int teeth_stopped, break_armed;
tick_t plat_now(void)
{
return now_t;
}
void plat_wait_until(tick_t t)
{
if (t > now_t)
now_t = t;
}
void plat_set_compare(tick_t t)
{
cmp_t = t;
}
static tick_t coil_since[CYL];
static double coil_max_ms;
static double t_meas;
void plat_pin(int ch, int level)
{
if (ch < CYL) {
if (level) {
coil_since[ch] = now_t;
if (break_armed && ch == 0 && (double)now_t / TICK_HZ > t_meas)
teeth_stopped = 1; /* обрыв датчика: катушка 1 заряжается, зубьев больше нет */
} else if (coil_since[ch] >= 0) {
double ms = (double)(now_t - coil_since[ch]) * 1000.0 / TICK_HZ;
if (ms > coil_max_ms)
coil_max_ms = ms;
coil_since[ch] = -1;
}
}
}
/* ── вывод чисел без printf с плавающей точкой ───────────────────────────── */
static char L[640];
static int lp;
static void add(const char *s)
{
while (*s && lp < (int)sizeof L - 2)
L[lp++] = *s++;
}
static void emit(void)
{
L[lp++] = '\n';
L[lp] = 0;
sim_out(L);
lp = 0;
}
static void addi(long long v)
{
char b[24];
int i = 0;
if (v < 0) {
add("-");
v = -v;
}
do {
b[i++] = (char)('0' + v % 10);
v /= 10;
} while (v);
while (i) {
char c[2] = {b[--i], 0};
add(c);
}
}
static void addf(double v, int dec)
{
long long m = 1;
for (int i = 0; i < dec; i++)
m *= 10;
long long x = llround(v * (double)m);
if (x < 0) {
add("-");
x = -x;
}
addi(x / m);
if (dec) {
add(".");
long long f = x % m;
for (long long d = m / 10; d; d /= 10) {
char c[2] = {(char)('0' + f / d % 10), 0};
add(c);
}
}
}
/* подпись и число одной строкой */
static void kf(const char *s, double v, int dec)
{
add(s);
addf(v, dec);
}
static void ki(const char *s, long long v)
{
add(s);
addi(v);
}
/* ── замеры ──────────────────────────────────────────────────────────────── */
static eng_t M;
typedef struct {
long n, far_n, over, inj;
double emin, emax, esum, far_max, dwmin, dwmax, imin, imax, over_emin;
} res_t;
static res_t R;
static long sparks_rev; /* для модели с отсечкой */
static double phase_now(void)
{
double th = eng_theta(&M, (double)now_t / TICK_HZ);
return fmod(th, 720.0);
}
static double wrap_err(double e)
{
while (e >= 360)
e -= 720;
while (e < -360)
e += 720;
return e;
}
static void on_spark(const ign_ev *e, int by_overdwell)
{
sparks_rev++;
if ((double)now_t / TICK_HZ < t_meas)
return;
double err = wrap_err(phase_now() - (double)e->spark.angle);
double dw = (double)(now_t - e->charge_t) * 1000.0 / TICK_HZ;
if (by_overdwell) {
if (R.over == 0 || err < R.over_emin)
R.over_emin = err;
R.over++;
if (dw > R.dwmax)
R.dwmax = dw;
return;
}
if (R.n == 0 || err < R.emin)
R.emin = err;
if (R.n == 0 || err > R.emax)
R.emax = err;
if (R.n == 0 || dw < R.dwmin)
R.dwmin = dw;
if (R.n == 0 || dw > R.dwmax)
R.dwmax = dw;
R.esum += fabs(err);
R.n++;
if (e->spark.offset > 6.0f) { /* во время переведено больше шага колеса */
if (R.far_n == 0 || fabs(err) > R.far_max)
R.far_max = fabs(err);
R.far_n++;
}
}
static void on_inj(const inj_ev *e)
{
if ((double)now_t / TICK_HZ < t_meas)
return;
double err = wrap_err(phase_now() - (double)e->planned_end);
if (R.inj == 0 || err < R.imin)
R.imin = err;
if (R.inj == 0 || err > R.imax)
R.imax = err;
R.inj++;
}
/* ── прогон одного опыта ─────────────────────────────────────────────────── */
enum { P_FLAT, P_RAMP, P_LIMIT };
typedef struct {
int profile;
double rpm0, rpm1, dur; /* разгон или сброс: от rpm0 до rpm1 за dur секунд */
double tail; /* сколько смотреть после профиля, с */
} scen;
static ecu_cfg base_cfg(int mode)
{
ecu_cfg c = {mode, 1, 4.0f, -400.0f, 3.0f, 7000.0f, 50.0f, 8};
return c;
}
static uint32_t irq0, run0, spin0;
static double lim_true_max, lim_true_min;
static long lim_revs_cut;
static void run(const scen *sc, const ecu_cfg *cfg)
{
ecu_init(cfg);
ecu_on_spark = on_spark;
ecu_on_inj = on_inj;
R = (res_t){0};
now_t = 0;
cmp_t = NEVER;
teeth_stopped = 0;
coil_max_ms = 0;
for (int i = 0; i < CYL; i++)
coil_since[i] = -1;
eng_init(&M, -361.0, sc->rpm0);
t_meas = eng_time_of(&M, 2160.0); /* три цикла разгона стенда: ЭБУ уже знает обороты */
if (sc->profile == P_RAMP)
eng_ramp(&M, t_meas, sc->dur, sc->rpm1);
if (sc->profile == P_LIMIT)
eng_accel(&M, t_meas, sc->rpm1);
double t_end = t_meas + sc->dur + sc->tail;
long k = 0;
int snap = 0;
sparks_rev = 2;
lim_true_max = 0;
lim_true_min = 1e9;
lim_revs_cut = 0;
for (;;) {
tick_t tt = NEVER;
if (!teeth_stopped) {
double th = -360.0 + 360.0 * (double)(k / TEETH_PER_REV) + 6.0 * (double)(k % TEETH_PER_REV);
double t = eng_time_of(&M, th);
if (t >= 0 && t <= t_end)
tt = (tick_t)llround(t * TICK_HZ);
}
if (tt == NEVER && !teeth_stopped)
break; /* опыт кончился: хвост очереди не смотрим */
if (cmp_t != NEVER && cmp_t <= tt) {
if (cmp_t > now_t)
now_t = cmp_t;
cmp_t = NEVER;
evq_isr(&g_q);
} else if (tt != NEVER) {
if (tt > now_t)
now_t = tt;
if (!snap && (double)tt / TICK_HZ >= t_meas) {
snap = 1;
irq0 = g_q.irq;
run0 = g_q.run;
spin0 = g_q.spin;
}
if (sc->profile == P_LIMIT && k % TEETH_PER_REV == 0 && (double)tt / TICK_HZ > t_meas) {
/* игрушечная модель: обороты растут, пока есть искры, и падают без них */
double fired = (double)(sparks_rev > 2 ? 2 : sparks_rev) / 2.0;
if (fired < 1)
lim_revs_cut++;
eng_accel(&M, (double)tt / TICK_HZ, fired * sc->rpm1 - (1 - fired) * 2 * sc->rpm1);
sparks_rev = 0;
double r = eng_rpm(&M, (double)tt / TICK_HZ);
if (r > lim_true_max)
lim_true_max = r;
if (lim_true_max > 7000 && r < lim_true_min)
lim_true_min = r;
}
ecu_tooth(tt);
k++;
} else {
break;
}
}
if (now_t < (tick_t)llround(t_end * TICK_HZ))
now_t = (tick_t)llround(t_end * TICK_HZ);
}
static const char *mode_name[3] = {"M0 от метки", "M1 от зуба", "M2 от зуба, мгновенные"};
static void report(const char *name, int mode)
{
add(name);
add(" | ");
add(mode_name[mode]);
ki(": искр ", R.n);
kf(", ошибка от ", R.emin, 3);
kf(" до ", R.emax, 3);
kf(" град, средняя ", R.n ? R.esum / (double)R.n : 0, 3);
ki("; дальше 6 град от зуба ", R.far_n);
kf(" до ", R.far_max, 3);
kf("; накопление ", R.dwmin, 3);
kf("...", R.dwmax, 3);
ki(" мс; по защите ", R.over);
if (R.over) {
kf(" (раньше плана до ", -R.over_emin, 3);
add(" град)");
}
ki("; отказов очереди ", (long long)g_st.refused);
ki("; впрыск ", R.inj);
kf(", конец от ", R.imin, 3);
kf(" до ", R.imax, 3);
add(" град");
emit();
}
int sim_main(void)
{
add("== 1. План по оборотам: опережение (getInitialAdvance, 100 кПа, 35°) и накопление 4 мс");
emit();
static const double rpms[] = {600, 1000, 2000, 3000, 6000, 7000};
for (int i = 0; i < 6; i++) {
ki("об/мин ", (long long)rpms[i]);
kf(": опережение ", (double)advance_deg((float)rpms[i]), 1);
kf(" град, накопление 4 мс = ", 4.0 / (1000.0 * 60 / 360 / rpms[i]), 2);
kf(" град, впрыск 3 мс = ", 3.0 / (1000.0 * 60 / 360 / rpms[i]), 2);
kf(" град, зуб 6 град = ", 6.0 * 1e6 / (rpms[i] * 6.0), 1);
add(" мкс");
emit();
}
static const struct {
const char *name;
scen s;
} list[] = {
{"ровно 3000", {P_FLAT, 3000, 3000, 1.0, 0}},
{"разгон 1000->7000 за 1 с", {P_RAMP, 1000, 7000, 1.0, 0.2}},
{"рывок 2000->6000 за 0,25 с", {P_RAMP, 2000, 6000, 0.25, 0.2}},
{"сброс 6000->1500 за 0,5 с", {P_RAMP, 6000, 1500, 0.5, 0.2}},
};
add("== 2. Ошибка момента искры и конца впрыска, градусы вала (плюс — позже плана)");
emit();
for (int i = 0; i < 4; i++)
for (int m = 0; m < 3; m++) {
ecu_cfg c = base_cfg(m);
run(&list[i].s, &c);
report(list[i].name, m);
}
add("== 3. Обрыв датчика на 3000 об/мин, пока заряжается катушка 1");
emit();
for (int od = 1; od >= 0; od--) {
ecu_cfg c = base_cfg(ECU_FROM_TOOTH);
c.overdwell = od;
scen s = {P_FLAT, 3000, 3000, 0.1, 0.05};
break_armed = 1;
run(&s, &c);
break_armed = 0;
add(od ? "защита 1,5 накопления: " : "без защиты: ");
ki("искр по защите ", R.over);
kf(", катушка под током до ", od ? R.dwmax : coil_max_ms, 3);
add(" мс");
if (!od) {
int on = 0;
for (int i = 0; i < CYL; i++)
on += coil_since[i] >= 0;
ki(", к концу опыта не погасли катушек ", on);
kf(", горит уже ", (double)(now_t - coil_since[0]) * 1000.0 / TICK_HZ, 1);
add(" мс");
}
emit();
}
add("== 4. Отсечка 7000 об/мин, возврат на 50 ниже; модель: +4000 об/мин/с с искрами, -8000 без");
emit();
{
ecu_cfg c = base_cfg(ECU_FROM_TOOTH);
scen s = {P_LIMIT, 6500, 4000, 1.0, 0};
run(&s, &c);
ki("срезано искр ", (long long)g_st.cut_spark);
ki(", впрысков ", (long long)g_st.cut_fuel);
ki("; оборотов без искр ", lim_revs_cut);
kf("; обороты ЭБУ за цикл до ", (double)g_st.rpm_max, 1);
kf("; настоящие обороты от ", lim_true_min, 1);
kf(" до ", lim_true_max, 1);
emit();
}
add("== 5. Очередь на 6000 об/мин за 1 с: 4 искры и 4 впрыска за цикл; конец впрыска -394,95 град: на 0,05 град позже искры соседа");
emit();
for (int late = 0; late <= 8; late += 8) {
ecu_cfg c = base_cfg(ECU_FROM_TOOTH);
c.late_us = late;
c.inj_end = -394.95f;
scen s = {P_FLAT, 6000, 6000, 1.0, 0};
run(&s, &c);
ki("задержка ", late);
ki(" мкс: прерываний таймера ", (long long)(g_q.irq - irq0));
ki(", событий ", (long long)(g_q.run - run0));
ki(", дождались в цикле ", (long long)(g_q.spin - spin0));
ki("; длина очереди до ", (long long)g_q.max_len);
ki(", слотов пула до ", (long long)g_q.max_pool);
ki(" из ", EVQ_POOL);
kf("; искра от ", R.emin, 3);
kf(" до ", R.emax, 3);
kf(" град, конец впрыска от ", R.imin, 3);
kf(" до ", R.imax, 3);
add(" град");
emit();
}
add("== конец");
emit();
return 0;
}
host19m.c — стенд на компьютере.
/* host19m.c — раздел 19м: стенд на компьютере, вывод в stdout. */
#include <stdio.h>
int sim_main(void);
void sim_out(const char *s)
{
fputs(s, stdout);
}
int main(void)
{
return sim_main();
}
qemu19m.c — стенд в QEMU: вывод через полухостинг.
/* qemu19m.c — раздел 19м: тот же стенд sim19m.c на Cortex-M4F в QEMU netduinoplus2.
* Вывод — через полухостинг (SYS_WRITE0), выход — SYS_EXIT. Часы стенда виртуальные,
* поэтому вывод обязан совпасть с прогоном на компьютере знак в знак. */
#include <stdint.h>
int sim_main(void);
static uint32_t semihost(uint32_t op, const void *arg)
{
register uint32_t r0 __asm("r0") = op;
register const void *r1 __asm("r1") = arg;
__asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory");
return r0;
}
void sim_out(const char *s)
{
semihost(0x04, s); /* SYS_WRITE0 */
}
int main(void)
{
sim_main();
semihost(0x18, (const void *)0x20026u); /* SYS_EXIT, ADP_Stopped_ApplicationExit */
for (;;)
;
}
start19m.c — стартовый код Cortex-M4F.
/* start19m.c — раздел 19м: стартовый код Cortex-M4F для STM32F407 и QEMU netduinoplus2 (STM32F405).
* Таблица векторов до TIM5 (прерывание 50, адрес 0x108 по RM0090), копия .data, обнуление .bss,
* включение FPU и вызов main. */
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void Default_Handler(void)
{
for (;;)
;
}
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void TIM5_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0;
*(volatile uint32_t *)0xE000ED88u |= 0xFu << 20; /* CPACR: CP10 и CP11 — полный доступ к FPU */
__asm volatile("dsb\n isb");
main();
for (;;)
;
}
__attribute__((section(".isr_vector"), used)) void (*const vectors[16 + 51])(void) = {
[0] = (void (*)(void))&_estack,
[1] = Reset_Handler,
[2] = Default_Handler, /* NMI */
[3] = HardFault_Handler,
[4 ... 15] = Default_Handler,
[16 ... 65] = Default_Handler,
[16 + 50] = TIM5_IRQHandler,
};
fw19m.c — платформа на TIM5 для STM32F407.
/* fw19m.c — раздел 19м: платформа для STM32F407 — один 32-битный TIM5 на всё время.
* 168 МГц от кварца 8 МГц, шина APB1 42 МГц, таймеры на ней — 84 МГц, делитель 21 -> 4 МГц.
* TIM5 CH2 (PA1, AF2) ловит зубья колеса захватом, CH1 — сравнение для очереди событий:
* зуб и событие меряются одним счётчиком, как в microsecond_timer_stm32.cpp у rusEFI.
* Катушки — PD12…PD15, форсунки — PE7…PE10. Собирается и меряется по размеру; на плате не запускалось. */
#include "stm32f4xx.h"
#include "ecu19m.h"
static uint32_t hi, last_lo;
/* 64-битное время из 32-битного счётчика: перенос ловим при каждом чтении
* (читать чаще, чем раз в 1073 с — у ЭБУ на работающем двигателе это так). */
tick_t plat_now(void)
{
uint32_t pm = __get_PRIMASK();
__disable_irq();
uint32_t lo = TIM5->CNT;
if (lo < last_lo)
hi++;
last_lo = lo;
tick_t t = ((tick_t)hi << 32) | lo;
__set_PRIMASK(pm);
return t;
}
void plat_wait_until(tick_t t)
{
while (plat_now() < t)
;
}
void plat_set_compare(tick_t t)
{
TIM5->CCR1 = (uint32_t)t;
TIM5->SR = ~TIM_SR_CC1IF;
TIM5->DIER |= TIM_DIER_CC1IE;
if ((int32_t)((uint32_t)t - TIM5->CNT) <= 0)
TIM5->EGR = TIM_EGR_CC1G; /* срок прошёл, пока писали регистр, — прерывание сразу */
}
void plat_pin(int ch, int level)
{
GPIO_TypeDef *port = ch < 4 ? GPIOD : GPIOE;
uint32_t pin = ch < 4 ? 12u + (uint32_t)ch : 7u + (uint32_t)(ch - 4);
port->BSRR = level ? 1u << pin : 1u << (pin + 16);
}
void TIM5_IRQHandler(void)
{
uint32_t sr = TIM5->SR;
if (sr & TIM_SR_CC2IF) {
uint32_t cap = TIM5->CCR2; /* чтение снимает флаг */
tick_t now = plat_now();
ecu_tooth(now - (tick_t)(uint32_t)((uint32_t)now - cap));
}
if ((sr & TIM_SR_CC1IF) && (TIM5->DIER & TIM_DIER_CC1IE)) {
TIM5->DIER &= ~TIM_DIER_CC1IE;
TIM5->SR = ~TIM_SR_CC1IF;
evq_isr(&g_q);
}
}
static void clock_168mhz(void)
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY))
;
FLASH->ACR = FLASH_ACR_LATENCY_5WS | FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN;
RCC->PLLCFGR = 8u << RCC_PLLCFGR_PLLM_Pos | 336u << RCC_PLLCFGR_PLLN_Pos | 0u << RCC_PLLCFGR_PLLP_Pos
| 7u << RCC_PLLCFGR_PLLQ_Pos | RCC_PLLCFGR_PLLSRC_HSE;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY))
;
RCC->CFGR = RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL)
;
}
static void io_init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOEEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER1) | GPIO_MODER_MODER1_1; /* PA1 — альтернативная */
GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFu << 4)) | 2u << 4; /* AF2 = TIM5_CH2 */
for (int i = 12; i <= 15; i++)
GPIOD->MODER = (GPIOD->MODER & ~(3u << 2 * i)) | 1u << 2 * i;
for (int i = 7; i <= 10; i++)
GPIOE->MODER = (GPIOE->MODER & ~(3u << 2 * i)) | 1u << 2 * i;
TIM5->PSC = 21 - 1; /* 84 МГц / 21 = 4 МГц */
TIM5->ARR = 0xFFFFFFFFu;
TIM5->EGR = TIM_EGR_UG;
TIM5->SR = 0;
TIM5->CCMR1 = TIM_CCMR1_OC1M_0 /* CH1: сравнение, «активный уровень при совпадении» */
| TIM_CCMR1_CC2S_0 /* CH2: захват с TI2 */
| TIM_CCMR1_IC2F_0 | TIM_CCMR1_IC2F_1; /* фильтр: 8 отсчётов подряд */
TIM5->CCER = TIM_CCER_CC2E; /* захват по фронту */
TIM5->DIER = TIM_DIER_CC2IE;
NVIC_SetPriority(TIM5_IRQn, 1);
NVIC_EnableIRQ(TIM5_IRQn);
TIM5->CR1 = TIM_CR1_CEN;
}
int main(void)
{
static const ecu_cfg cfg = {ECU_FROM_TOOTH, 1, 4.0f, -400.0f, 3.0f, 7000.0f, 50.0f, 8};
clock_168mhz();
ecu_init(&cfg);
io_init();
for (;;)
__WFI();
}
f407_19m.ld — скрипт компоновщика.
/* f407_19m.ld — раздел 19м: STM32F407VG и QEMU netduinoplus2 (STM32F405): 1 МБ флеш-памяти, 128 КБ ОЗУ. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(8); _ebss = .; } > RAM
end = .;
}
run19m.sh — прогон в контейнерах stm-sys:1 и stm-arm:1.
#!/bin/sh
# Раздел 19м: прогон в два шага. /v — каталог rusefi19m (только чтение); /di, /ci — заголовки CMSIS
# (STM32F4xx DFP 2.17.1 и CMSIS 5.9.0, только чтение).
# sh /v/run19m.sh host в stm-sys:1 — стенд на компьютере: модель вала + ЭБУ
# sh /v/run19m.sh arm в stm-arm:1 — сборка под STM32F407 (размеры) и тот же стенд в QEMU netduinoplus2
set -u
export LC_ALL=C.UTF-8
W=/tmp/w19m
rm -rf $W
mkdir -p $W
cd $W
LIB="evq19m.c ang19m.c ecu19m.c"
SIM="eng19m.c sim19m.c"
WARN="-std=c11 -Wall -Wextra -Wdouble-promotion -ffp-contract=off"
src() { for f in "$@"; do printf '/v/%s ' "$f"; done; }
case "$1" in
host)
echo "== версии"
gcc --version | head -1
echo "== сборка стенда"
gcc -O2 $WARN -fsanitize=undefined -fno-sanitize-recover=all -I/v $(src $LIB $SIM host19m.c) -lm -o sim 2>&1
echo "build_host=$?"
echo "== прогон"
./sim
echo "host_exit=$?"
;;
arm)
echo "== версии"
arm-none-eabi-gcc --version | head -1
qemu-system-arm --version | head -1
M4="-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard"
OS="-Os $WARN -ffunction-sections -fdata-sections"
echo "== объектные файлы под Cortex-M4F, -Os"
bad=0
for f in $LIB fw19m.c start19m.c; do
arm-none-eabi-gcc $M4 $OS -DSTM32F407xx -I/v -I/di -I/ci -c /v/$f -o ${f%.c}.o 2>&1 || bad=1
done
echo "build_obj=$bad"
arm-none-eabi-size evq19m.o ang19m.o ecu19m.o fw19m.o start19m.o
echo "вызовы double-библиотеки в evq19m/ang19m/ecu19m: $(arm-none-eabi-objdump -d evq19m.o ang19m.o ecu19m.o | grep -c 'bl.*__aeabi_d')"
echo "команды FPU в ecu19m.o: $(arm-none-eabi-objdump -d ecu19m.o | grep -cE '\sv(add|sub|mul|div|cmp|cmpe|cvt|mov|ldr|str|neg|abs)')"
echo "== прошивка fw19m.elf"
arm-none-eabi-gcc $M4 $OS -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections \
-T /v/f407_19m.ld evq19m.o ang19m.o ecu19m.o fw19m.o start19m.o -lm -o fw19m.elf 2>&1
echo "build_fw=$?"
echo "fw19m.elf: $(arm-none-eabi-size fw19m.elf | tail -1 | awk '{print "text " $1 ", data " $2 ", bss " $3}')"
lib() { arm-none-eabi-nm "$1" | awk '$3 ~ /^__aeabi_(f2lz|l2f|dmul|dadd)$/ {printf "%s ", $3}'; }
echo "библиотечные float/int64 и double в fw19m.elf: [$(lib fw19m.elf)]"
echo "== то же с прямым преобразованием float <-> int64 (-DTICK_CAST64)"
mkdir c64
for f in $LIB fw19m.c start19m.c; do
arm-none-eabi-gcc $M4 $OS -DTICK_CAST64 -DSTM32F407xx -I/v -I/di -I/ci -c /v/$f -o c64/${f%.c}.o 2>&1 || bad=1
done
arm-none-eabi-gcc $M4 $OS -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections \
-T /v/f407_19m.ld c64/*.o -lm -o fw64.elf 2>&1
echo "build_fw64=$?"
echo "fw64.elf: $(arm-none-eabi-size fw64.elf | tail -1 | awk '{print "text " $1 ", data " $2 ", bss " $3}')"
echo "библиотечные float/int64 и double в fw64.elf: [$(lib fw64.elf)]"
echo "== тот же стенд в QEMU netduinoplus2 (Cortex-M4F)"
arm-none-eabi-gcc $M4 -O2 $WARN -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections \
-I/v $(src $LIB $SIM qemu19m.c start19m.c) -T /v/f407_19m.ld -lm -o q19m.elf 2>&1
echo "build_qemu=$?"
timeout 600 qemu-system-arm -M netduinoplus2 -nographic -semihosting-config enable=on,target=native \
-kernel q19m.elf -monitor none -serial none > q.txt 2>&1
echo "qemu_exit=$?"
echo "== вывод QEMU"
tr -d '\r' < q.txt
;;
esac
Дальше: раздел 19н «CAN-контроллер MCP2515 по SPI: когда в микроконтроллере нет bxCAN, битовое время на 8 МГц, буферы, фильтры и прерывания» — CAN без bxCAN: внешний контроллер на SPI для платы, где CAN занят или его нет.