Developer information for timing from the last possible tooth

is implemented in stable1_1 branch (eg. 1.0.63 firmware release).

Should follow soon: (these are supported by the old firmware, but not yet supported by the overlapping dwell code)

Test cases

There are many setups to test, eg.


This means many combinations.

We added test-code that verifies the dwell, ignition advance, overlap etc... so less manual verification is necessary.

Good sides:

But it's still limited in several ways.

The target is to remove limitations, add support for practically the most exotic configurations with precision unthinkable for competing ECMs.

The good-old multitooth code is at the very core we build on

Simple m-n tooth wheel decoding has been operational since October 2003. However even though the simplistic implentation worked good in practice, it had the same drawbacks as a simple trigger: the fixed distance in crank degrees from trigger to spark-fire, makes it sensitive to changes in rpm. Compensating with the derivative of rpm helps the problem, but doesn't solve all problems.

The intention of this page is to describe how the fixed distance is reduced, by trigging from the last tooth possible on the multitooth trigger wheel.

Kindergarden review - basic specifications

To architecture a good toothwheel interface, let's try to collect the demand: basically the end of known length (dwell and injector-pulsewidth) pulses need to be timed to exact engine-position. If the RPM changes somewhat (this is generally a very small effect, generally neglected), this will not be dead-sharp:

InputTrigger/TriggerLog showed that RPM variations can be surprisingly big even within 100 degrees of crank rotation. This justifies the "timing from last possible tooth" approach. However this makes little difference in the output of fuel and ignadv calculations, so there is no need to make sure they are done within the last few degrees (last 250 degrees is very fine)

The trig from last tooth consists of:

tooth wheel decoding

[multitooth.c: tooth_detection()]

Measure the time t between two interrupts (teeth). If t is significantly longer then the previous t, then the missing tooth has been found. This is by tooth_detection() signalled by the return variable. One flag (in the return variable) of particular interest is when the missing tooth is found for the first time (eg. during the synchronization), see engine phase.

engine phase

[timing.c: update_engphase()]

A variable engine.engphase holds the engine phase. While the crank rotates 0..720 degrees (2 crank rotations), it will go round and round, 8 bit is enough:
  • 0..215 (mod216 for 36 tooth, += 3 at every tooth)
  • 0..239 (mod240 for 60 tooth, += 2 at every tooth)
  • config.reset_enginephase_after in general
When tooth_detection() signals that the missing tooth is found during synchronization, the engine phase variable is initialized with the value of config.engphase_sync. At the same time, igncount is initialized and the first (trigger phase, trig2spark) set is loaded.

[timing.c: engphase_trig()]

If the engine phase matches the precalculated trigger phase, the ignition schedule handler will be activated.

[timing.c: next_engphase_trigger()]

After the ignition handler has scheduled the IGNDEACT event, the variable igncount is updated and the next (trigger phase, trig2spark) set is loaded. This is the only place where engine.igncount is modified (except update_engphase() during tooth wheel synchronization), thus synchronization issues is non-existing.

trigger phase calculation

[ve.c: calc_trigger()]

The purpose of calc_trigger() is to lookup at what engine phase the tdc will occur (from the tdc-engphase table), and compute how many, N, teeth are needed to step back from tdc, such that N*[degrees/tooth] >= ignition advance. In case the computed engine phase corresponds to the missing tooth, further back stepping will be done until an engine phase that matches a real tooth is found.

[ve.c: decrease_engphase()]

Helper function; decreases engine phase and handles overflow.

[ve.c: phase_in_missing_tooth()]

Helper function; returns the distance (in engine phase units) from suggested trigger phase to the tooth before the missing tooth. In case the suggested trigger phase doesn't overlap the missing tooth, nothing is done.

minor syntax question: should we use a typedef for engphase variables? (but uint8_t for now). Both for clarity and for future (maybe it worths to use more bits for some reason on other processors).

Dwellstart overview

With the timing from last tooth, the early_dwellstart can be activated at any low RPM if the dwelltooth is not determined independently: not good (dwell might be longer than desired).

Dwellstart for the most generic case is somewhat more complex than igndeact.

TAFD - time available for dwell (TAFD) is particularly interesting:

We calculate TAFD at the same time we calculate proposed_dwellphase and proposed_trig2dwell. h[2] must be traversed to see when the same output channel is fired. The relevant elements from h[1] must be looked up, and the difference is the TAFD itself.


This same algorithm applies for COP, wasted spark, distributor or oddfire-V6.


JTune CVS org.vemsgroup.firmware.engine.UserspaceCalc.nextIndexWithSameIgnout()


As you see, there are 3 scenarios, represented with 2 flags: dd2ds (dwellstart => spark), ds2dd (spark => dwellstart)


where to decide ?

Ignition state machine in detail - do we keep the explicite ignstate[] array and what is the exact state-machine

Note that the ignstate might be redundant with the

However there might be benefits from having explicit ignition state (array):

The decoupled (non-independent) flags that are of interest:

Userspace suggestions or in-irq decisions that effect the actuation:

The new ignition-state concept (more spectacular than the old, but still only 5 states) is discussed in detail:

JTune CVS org.vemsgroup.firmware.engine.IgnitionState class


Only 1 channel is allowed to start/schedule dwell or spark from any given tooth. This has always been this way. Actually, it only takes a "while" line around the engphase_trig(dwell..); engphase_trig(spark..); actions, but I don't know where it's needed. Maybe it could be a my_make option.

See also