19м. rusEFI from the inside: spark and injection by crank angle — dwell, advance and an event queue on a single timer
In section 19з the 60-2 wheel decoder turned the sensor edges into the cycle phase and the engine speed. This section covers what the unit does next: how the angle "spark 35° before top dead center" becomes the moment in time when the coil must be switched on so that it has time to build up current, when the injector must open, how a single hardware timer keeps up with all of it, and what breaks when the engine accelerates or drops speed between teeth.
Next to the rusEFI code there is our own scheduler in C with the same structure, and a model of a crankshaft whose speed changes: on it three ways of scheduling events are compared. The test bench was run on a PC and, from the same source, in QEMU on a Cortex-M4F; the STM32F407 firmware on TIM5 was built and measured, but it was not run on a board.
Sources: the rusEFI code — github.com/rusefi/rusefi (commit 79ceeb32),
license GPL-3.0 with three reservations in license.txt (not for use on public
roads, not for emissions compliance, not for manned aviation);
quotes are a few lines each, with the file path. Our own code is written from scratch: it
reproduces the structure of rusEFI, not its text. Documents: RM0090 (interrupt
vectors and timer clocking of the STM32F407), DS8626 (pin alternate
functions).
Terms used in the documentation
Term Meaning TDC, BTDC, ATDC top dead center, before TDC, after TDC engine cycle, engine phase engine cycle (720°); phase is the angle within the cycle ignition timing, advance the moment of ignition, the advance angle dwell, overdwell the coil is energized before the spark; charging it too long injection phase, offset; timing mode End / Start / Center injection phase: angle of the end / start / middle of the pulse injector pulse width how long the injector is held open event queue, scheduler, executor a queue of events ordered by time, the scheduler output compare an interrupt when the timer counter reaches a set value late delay, spin wait queue delay: wait for a near event in a loop angle-based event, trigger scheduler an event by angle: it waits for "its" tooth rev limiter, hysteresis fuel/spark cut, resuming below the threshold
19м.1 From tooth to spark: what the unit does after the decoder
Every tooth arrives in mainTriggerCallback()
(firmware/controllers/engine_cycle/main_trigger_callback.cpp): the edge
time in 4 MHz ticks, the phase of this tooth (currentPhase) and of the next one
(nextPhase). If there is no engine speed yet (rpm == 0), the function returns. Otherwise there are
three steps:
// 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);
Fuel — the injectors whose opening falls between this tooth and the next one; then the events that were waiting for this tooth; then the coils whose dwell starts before the next tooth. The key idea: on a tooth, only what will happen before the next tooth is scheduled. From the tooth to the angle there is less than one wheel step (6° on a 60-2 wheel, 18° across the gap), and only this remainder is converted to time using the engine speed. A speed error is multiplied by a few degrees, not by the whole cycle.
The bench engine: four cylinders, firing order 1-3-4-2, compression TDC at 0, 180, 360 and 540° of the cycle, one coil and one injector per cylinder; the cycle phase is assumed known, as with a camshaft sensor.
19м.2 The spark plan: advance and dwell
rusEFI takes the advance angle from the ignitionTable table by engine speed
and load. The default table is built by buildTimingMap(35) in
controllers/algo/defaults/default_ignition.cpp: getAdvanceForRpm()
gives 10° below 600 rpm, 35° from 3000 and a straight line between them,
getInitialAdvance() adds 0.1 * (100 - map) — zero at 100 kPa in the
intake. The bench's advance_deg() reproduces this straight line.
Dwell is a time: by default setConstantDwell(4), 4 ms at any
engine speed; the dwell angle is the time
divided by the time of one degree (ignition_state.cpp). The cylinder angles are computed by
prepareCylinderIgnitionSchedule() in 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);
and dwellStartAngle = sparkAngle - dwellAngleDuration. On the bench:
/* План искры: угол искры = ВМТ − опережение, накопление — заданное время,
* переведённое в градусы по оборотам на момент плана. */
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));
}
| Speed, rpm | Advance | Dwell 4 ms | Injection 3 ms | Tooth 6° |
|---|---|---|---|---|
| 600 | 10.0° | 14.40° | 10.80° | 1666.7 µs |
| 1000 | 14.2° | 24.00° | 18.00° | 1000.0 µs |
| 2000 | 24.6° | 48.00° | 36.00° | 500.0 µs |
| 3000 | 35.0° | 72.00° | 54.00° | 333.3 µs |
| 6000 | 35.0° | 144.00° | 108.00° | 166.7 µs |
| 7000 | 35.0° | 168.00° | 126.00° | 142.9 µs |
At 7000 rpm the dwell is 168°, 28 wheel steps: the coil starts
charging long before the spark, and dozens of teeth go past. The plan
is recalculated after every spark: fireSparkAndPrepareNextSchedule()
switches the coil off and calls prepareCylinderIgnitionSchedule() for the next
cycle; on the bench this is fire().
19м.3 A time queue on a single timer
All events meet in one queue: EventQueue (controllers/system/timer/event_queue.cpp)
and SingleTimerExecutor (single_timer_executor.cpp). It is a list sorted
by time with linear insertion (the comment says there are few events),
a pool of 64 slots (m_pool[64]) for "fire and forget" events, and one
timer in compare mode, armed for the head of the list. On the STM32 this is
TIM5 (port_mpu_util.h: SCHEDULER_TIMER_DEVICE TIM5, 4 MHz); the same
counter is the clock of the whole firmware (getTimeNowLowerNt() reads TIM5->CNT),
so the tooth time and the event time are measured with the same ruler.
The head of the queue has two thresholds. The queue delay:
// 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))
If the event is less than 8 µs away, the handler waits for it in a loop:
// 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();
}
And the lower bound: setHardwareSchedulerTimer() (microsecond_timer.cpp)
does not arm the timer closer than US2NT(2) — otherwise the deadline would end up in the past.
The bench reproduces both:
/* Прерывание сравнения: выполнить всё, чей срок наступил или наступит
* в пределах 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() arms the compare for the head: a late event fires after
the queue delay, and never closer than 2 µs. The slot is released before the
action is called, so that the action can schedule itself again. A slot that is already
in the queue is not scheduled a second time — as in insertTask() in rusEFI
(if (scheduling->action) → return false).
Experiment: is the delay needed. 6000 rpm, 1 s. The end of injection is deliberately set to −394.95°: 0.05° later than the spark of the neighboring cylinder, that is, 1.4 µs later — closer than 2 µs:
задержка 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 град
Without the delay every event gets its own interrupt, and the injector closing slips to 2 µs after the spark: +0.020°. With an 8 µs delay, 200 closings were waited for in the loop and came on time, with 599 interrupts instead of 799. The cost: the CPU spins in the handler for up to 8 µs. The queue is never longer than 3 events, and no more than 2 of the 32 pool slots are in use.
19м.4 Angle to time: "wait for your tooth"
The conversion of angle to time is scheduleByAngle() in rpm_calculator.cpp:
delayUs = oneDegreeUs * angle, where oneDegreeUs is the time of one degree at the current
engine speed, and schedule() at nowNt plus the offset. Time is 64-bit, but the offset is
cast to 32 bits by sumTickAndFloat() (util/efitime.h): return ticks
+ (int32_t) extra; — the comment explains that the offset is less than 10 s,
and 32 bits are enough for 500 s of ticks. Why — see 19м.8.
Angle-based events are handled by 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() is isPhaseInRange(angle, currentPhase, nextPhase):
whether the angle falls between this tooth and the next one, with the wrap 720 → 0. If
not, the event waits in the list, and on every tooth
scheduleEventsUntilNextTriggerTooth() converts to time everything that is due.
On the bench the same is done by phase_in_range(), sched_by_angle(),
ang_schedule_or_queue() and ang_on_tooth() (ang19m.c); the offset in ticks goes
through int32_t (the F2T macro in plat19m.h). Ignition on a tooth in full:
/* Как 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 Injection: open by angle, close by time
The injector opening is scheduled by angle, the closing by a duration after the
opening (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 means a slot from the pool. A pulse shorter than 50 µs is not scheduled: the events
could swap places, and the injector would stay open. The opening angle
is computed by computeInjectionAngle() (fuel_schedule.cpp). The phase injectionOffset is in degrees after compression
TDC, by default PORT_INJECTION_OFFSET −400° (defaults.h): injection
ends on the exhaust stroke, onto a closed intake valve. In the
InjectionTimingMode::End mode the opening is shifted back by the whole duration:
// firmware/controllers/engine_cycle/fuel_schedule.cpp (rusEFI, GPL-3.0)
angle_t openingAngle = injectionOffset - injectionDurationAngle;
The bench uses the End mode with a 3 ms pulse:
/* Импульс форсунки: открыть по углу, закрыть по времени. */
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);
}
The angle of the next pulse is recalculated on closing (in rusEFI
turnInjectionPinLow() calls event->update(), on the bench inj_close() calls
plan_inj()), that is, using the engine speed of the previous cycle.
19м.6 Acceleration and deceleration between teeth: three methods on the model
The crankshaft model (eng19m.c) sets the engine speed as segments with constant
acceleration; the angle and the time of reaching an angle are computed exactly, in double;
the 60-2 teeth come from the same model. The clock is virtual — time jumps to
the nearest tooth or timer compare. When the ECU switches a coil off, the bench
looks at the angle the shaft is actually at and subtracts the planned angle.
A plus means the spark is later than planned.
The methods: from the sync mark — on the first tooth after the gap the whole cycle ahead
is converted to time using the speed over the cycle; from the tooth — as rusEFI does,
with the speed averaged over the last 720°; from the tooth, instantaneous — speed over the
last 90°, as with the rusEFI setting alwaysInstantRpm:
// firmware/controllers/engine_cycle/rpm_calculator.cpp (rusEFI, GPL-3.0)
if (alwaysInstantRpm) {
rpmState->setRpmValue(instantRpm);
The measurement starts after three cycles, when the ECU already knows the speed; dwell 4 ms, pulse 3 ms, guard enabled.
| Experiment | Method | Sparks | Spark error, ° | Mean, ° | Dwell, ms | By guard | Refused | End of injection, ° |
|---|---|---|---|---|---|---|---|---|
| acceleration 1000 → 7000 in 1 s | from the mark | 134 | 0.110…249.077 | 19.489 | 3.447…4.000 | 0 | 8 | 1.561…298.333 |
| acceleration 1000 → 7000 in 1 s | from the tooth | 141 | −0.005…2.099 | 0.144 | 2.047…3.997 | 0 | 0 | 1.545…18.985 |
| acceleration 1000 → 7000 in 1 s | from the tooth, instantaneous | 141 | −0.012…0.144 | 0.009 | 2.589…4.000 | 0 | 0 | −0.020…10.050 |
| snap 2000 → 6000 in 0.25 s | from the mark | 69 | 0.002…161.192 | 20.443 | 4.000…4.000 | 0 | 6 | −0.002…231.043 |
| snap 2000 → 6000 in 0.25 s | from the tooth | 73 | 0.002…2.241 | 0.211 | 2.370…4.000 | 0 | 0 | −0.002…27.320 |
| snap 2000 → 6000 in 0.25 s | from the tooth, instantaneous | 73 | −0.006…0.150 | 0.011 | 2.906…4.000 | 0 | 0 | −0.002…14.486 |
| deceleration 6000 → 1500 in 0.5 s | from the mark | 72 | −155.405…−0.001 | 24.682 | 4.000…4.000 | 0 | 0 | −158.419…−0.414 |
| deceleration 6000 → 1500 in 0.5 s | from the tooth | 69 | −1.258…−0.001 | 0.108 | 4.012…6.266 | 3 | 0 | −20.856…−0.387 |
| deceleration 6000 → 1500 in 0.5 s | from the tooth, instantaneous | 72 | −0.092…0.000 | 0.011 | 4.000…5.586 | 0 | 0 | −10.916…−0.001 |
At a steady 3000 the methods are equal: 100 sparks, error −0.002° — rounding
to the 0.25 µs tick and float precision.
From the mark is unusable whenever the speed changes. During acceleration the error reaches 249.077° — more than the 180° between cylinders; slots from the previous cycle are still waiting for their deadline when the new mark arrives: 8 queue refusals, 134 sparks instead of 141. During deceleration the spark comes earlier than planned, with an error of up to −155.405°.
From the tooth — degrees instead of hundreds of degrees: up to 2.099° during acceleration, up to 2.241° on the snap. Not a single spark was converted to time further than 6° from a tooth; the remaining error comes from the speed: the average over 720° lags by almost a cycle. Instantaneous speed removes that too: up to 0.144° during acceleration, 0.150° on the snap, −0.092° during deceleration.
Dwell and injection are vulnerable with any method. The dwell angle was computed at the previous spark; during acceleration the degrees pass faster — the coil charges for 2.047 ms instead of 4 (instantaneous — 2.589 ms), during deceleration for up to 6.266 ms. The end of injection during acceleration is late by up to 18.985° (instantaneous — 10.050°).
19м.7 The overdwell guard and the rev limiter
The guard. A spark that waits for a tooth may never get it: the sensor
wire breaks, the engine stalls — and the coil stays energized. If the spark
went into the waiting list, scheduleSparkEvent() (spark_logic.cpp)
puts a spark "by the clock" into the same slot:
// 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));
If the tooth arrives, scheduleEventsUntilNextTriggerTooth() removes the guard
and schedules the spark by angle; if it does not, overFireSparkAndPrepareNextSchedule()
switches the coil off (on the bench — coil_overdwell()). Experiment: 3000 rpm, the teeth
disappear when coil 1 has started charging:
защита 1,5 накопления: искр по защите 1, катушка под током до 6.000 мс
без защиты: искр по защите 0, катушка под током до 4.000 мс, к концу опыта не погасли катушек 1, горит уже 115.9 мс
With the guard the coil was switched off after 6 ms (1.5 × 4). Without it the coil stays on until the end of the experiment — 115.9 ms — and beyond, until the teeth come back. The flip side: on the 6000 → 1500 deceleration with the "from the tooth" method the guard fired 3 times — the dwell angle was computed at the still-high speed, the shaft slowed down, the 6 ms ran out before the spark tooth, and the spark fired earlier than planned by 9.337°. With instantaneous speed — not once.
The rev limiter. LimpManager (controllers/limp_manager.cpp) allows or
forbids fuel and spark. By default (default_base_engine.cpp)
rpmHardLimit = 7000, rpmHardLimitHyst = 50, and both are cut:
// 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) sets the cut above 7000 and clears it
below 6950; it is updated by onFastCallback() — the fast loop
(FAST_CALLBACK_RATE is Period200hz in main_loop.h), not by a tooth. While
the cut is active, a dwell does not start, but one already started is carried through to the spark so that the
coil does not burn out. The bench checks the threshold once per cycle:
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;
}
A toy model: with sparks the speed rises by 4000 rpm per second, without sparks it falls by 8000; start at 6500, 1 s:
срезано искр 72, впрысков 72; оборотов без искр 42; обороты ЭБУ за цикл до 7087.4; настоящие обороты от 6717.9 до 7100.0
The hysteresis is 50 rpm, yet the shaft swings from 6717.9 to 7100.0: the decision is made once per cycle, based on the average speed over the previous cycle.
19м.8 Firmware for the STM32F407 and the same bench in QEMU
fw19m.c: time and the queue, as in rusEFI, on a single 32-bit TIM5; in this firmware the teeth
are caught by input capture on the same timer. 168 MHz from an 8 MHz crystal,
APB1 is 42 MHz, the timers on it get double that, 84 MHz (RM0090), prescaler 21 →
4 MHz. Channel 2 (PA1, AF2 per DS8626) captures the teeth, channel 1 does the compare.
The TIM5 vector is number 50 (RM0090; TIM5_IRQn = 50 in stm32f407xx.h).
Compare mode OC1M = 001, as in 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; /* срок прошёл, пока писали регистр, — прерывание сразу */
}
If the deadline passed while CCR1 was being written, there will be no match until the next
counter wrap (1073 s), so the event is requested in software
(EGR = CC1G); rusEFI has such a branch for the AT32F4.
Build: 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
| Build | Code (text) | RAM (bss) | Library functions |
|---|---|---|---|
fw19m.elf, offset via int32_t |
4592 | 2088 | none |
fw64.elf, -DTICK_CAST64 |
6824 | 2088 | __aeabi_dadd, __aeabi_dmul, __aeabi_f2lz, __aeabi_l2f |
There are no double library calls in evq19m, ang19m and ecu19m;
ecu19m.o has 140 FPU instructions. A direct float → int64_t conversion
(-DTICK_CAST64) pulls in __aeabi_f2lz, __aeabi_l2f, and with them
__aeabi_dadd and __aeabi_dmul: +2232 bytes and software
double arithmetic in the tooth handler. That is why sumTickAndFloat() exists.
The same sim19m.c was built for the Cortex-M4F (-O2) and run in QEMU 7.2.22
on netduinoplus2 (STM32F405, the same core) with output through semihosting.
The bench clock is virtual, and the output matched the PC character for character in
all five experiments: float on the FPU computes the plan and the angle conversion the same way.
TIM5 capture and compare do not take part in this run.
19м.9 Pitfalls
The spark drifts by tens of degrees during acceleration. The whole cycle was scheduled from the sync mark — schedule only up to the next tooth.
A weak spark during acceleration. The dwell angle was computed from the old speed (2.047 ms instead of 4); instantaneous speed helps, but not completely.
On closing the throttle the spark is 9.337° early. The overdwell guard fired. The cure is accurate engine speed, not disabling the guard.
An event 1–2 µs after its neighbor is late. The timer is not armed closer than 2 µs — wait for near events in a loop (queue delay).
An event silently fails to get into the queue. Its slot is still waiting for the previous deadline;
in rusEFI insertTask() returns false.
float → int64 pulls in double. An offset under 536 s fits
in int32_t of 4 MHz ticks — cast to that.
The rev limiter "saws" wider than the hysteresis. The decision is made once per cycle from the average speed.
A compare is missed while the register was being written. Check the deadline against the counter
and, if it is late, set CC1G.
19м.10 Summary
- On every tooth rusEFI schedules only the events before the next tooth; only the remainder, less than one wheel step, is converted to time.
- The default advance goes from 10° at 600 rpm to 35° from 3000, dwell is 4 ms; at 7000 rpm that is 168° of the cycle.
- One queue on a single 32-bit TIM5, 4 MHz; events closer than 8 µs are waited for in a loop, the timer is never armed closer than 2 µs. At 6000 rpm — 799 events in 599 interrupts, the queue is never longer than 3.
- Acceleration 1000 → 7000 rpm in 1 s, spark error: from the mark — up to 249.077°, from the tooth — up to 2.099°, with instantaneous speed — up to 0.144°.
- Dwell during acceleration — 2.047 ms, end of injection late by 18.985°; with instantaneous speed — 2.589 ms and 10.050°.
- The 1.5 × dwell guard switches the coil off after 6 ms if the sensor is lost; on a sharp deceleration the same guard causes 3 early sparks (up to 9.337°).
- Rev limiter 7000/6950, once per cycle: speed 6717.9…7100.0.
- F407 firmware: 4592 bytes of code, 2088 bytes of RAM, no
double; with a directfloat→int64_t— 6824 bytes. The bench in QEMU matched the PC character for character.
Full source code
Directory _verify/rusefi19m/; copies of the quoted rusEFI files with license.txt are in rusefi/.
plat19m.h — what the queue and the ECU expect from the platform: time, timer, outputs.
/* 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 — the time-ordered event queue.
/* 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 — the event queue: insertion, cancellation, the compare interrupt.
/* 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 — events by crank angle.
/* 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 — events by angle: "wait for your tooth" and converting the remainder to time.
/* 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 — the ECU: settings, spark and injection events, counters.
/* 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 — the ECU: decoder, engine speed, plan, spark, injection, rev limiter.
/* 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 — the crankshaft model.
/* 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 — the crankshaft model: segments with constant acceleration.
/* 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 — the bench: virtual clock, experiments and measurements.
/* 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 — the bench on a PC.
/* 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 — the bench in QEMU: output through semihosting.
/* 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 startup code.
/* 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 — the TIM5 platform for the 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 — the linker script.
/* 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 — the run in the stm-sys:1 and stm-arm:1 containers.
#!/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
Next: section 19н "MCP2515 CAN controller over SPI: when the microcontroller has no bxCAN, bit timing at 8 MHz, buffers, filters and interrupts" — CAN without bxCAN: an external controller on SPI for a board where CAN is busy or missing.