Developer information for timing from the last possible tooth
I will need some review help and scoping. There are some delays due to unexpected events.
Shame as it is, but this is still not completed (or released), lives in trunk, not in stable branch. The truth is that we're still waiting for the ARM chips that is the most motivation for the trunk branch (although the same code will be used for v3.x design, that will continue to sell besides the ARM). Anyway, we want immediate support for the most desired 5-cyl at least with 60-2 crankwheel and camsync. See GenBoard/UnderDevelopment/FirmWare/TriggerRelated
Apparently working model commited in JTune CVS. Please review. Alexander, let me note that this is the (probably only) path to rotary.
- IgnitionState class is the most important. Surprisingly simple
- userspace calc (that calculates proposed phase) needs some tweaking so it works for all cases and testcases.
Test cases
There are many setups to test, eg.
- odd-v6 [output example] In the example, dwell is overlapping between cylinders 90 degrees apart, but not overlapping between cylinders 150 degrees apart.
- rotary
- 4..16cyl wasted spark
- 4..8cyl COP spark
throughout
- RPM ranges (including continuously changing RPM so the proposed_spark_tooth changes)
- ignition advance
- dwell
- position of missing tooth.
This means many combinations.
Let's decide on some canonical file format that we store the automatically generated test outputs for acknowledgement.
Note that the first period starts with all channels inactive so it might make sense to store starting from a 2nd (or whichever) rotation.
We can also add some test-code that verifies the dwell, ignition advance, overlap etc... so less manual verification is necessary.
Genboard has the most sophisticated ignition implementation of all opensource ECM.
Good sides:
- multitooth support: InputTrigger/MultitoothTriggerWheel
- basic camsync support (with some limitations)
But it's still limited in several ways.
- oddfire V6
- rotary
- GenBoard/Manual/InputTriggerCamSync condition could be dropped
- the best possible precision with fastest accelerating engines
The target is to remove limitations, add support for practically the most exotic configurations with precision unthinkable for competing ECMs.
A good reason for hurry is that current implementation is not explained in enough detail (or easily configurable in MegaTunix) with many example configs, and it's better investment to explain (make easily configurable in MegaTunix) the new system.
This improvement had been planned to be implemented early 2004, so it's really time.
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:
- for injection we insist on the pulsewidth. If RPM changes in the meantime, the injection will not be ending sharply timed to the valve-close, but measured quantity is exact. (but we make the end of injection configurable, of course)
- for ignition we insist on the precise end of the pulse timing (because that releases spark) even if the length of the pulse will be somewhat off (should the RPM change somewhat in the meantime).
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:
- m-n tooth wheel decoding
- engine phase update
- actuation according engine phase and state machine
- trigger phase calculation (userspace)
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.
- calculation of proposed_trig2dwell[] and proposed_dwellphase[]: similar as calc of proposed_trig2spark[] (in ve.c)
- actuation: DONE. same way as deact is scheduled: using condition engphase_trig(trigger1.dwell_phase)
- early_dwell: for every spark (dispatcher igndeact), dwellstart must be scheduled right away if
- the time available for dwell is less than 200 usec + desired dwell
- or the next dwellstart should happen earlier than the next tooth comes.
TAFD - time available for dwell (TAFD) is particularly interesting:
- h[2]=02 03 02 03 (TAFD = 360 crankdegrees)
- h[2]=05 04 02 03 (TAFD = 720 crankdegrees)
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.
notes:
- assuming ignition advance is the same
- overflow must be considered, namely adding config.reset_engphase_after if needed
This same algorithm applies for COP, wasted spark, distributor or oddfire-V6.
Check \n
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)
- 1,0 very low RPM: same tooth schedules act, and fills sparkat: act schedules deact, same 0-1-2 as the simple trigger we always had (dwell_phase == spark_phase but there are no tooth between them)
- 0,0 normally a tooth schedules act, and a later tooth schedules deact. dwell_phase != spark_phase
- 0,1 at very high RPM the deact can schedule next act: (3-4-2, or early dwellstart) dwell_phase == spark_phase, and there are tooth between them (these are found automaticallyl when looking for the dwell_phase)
Issues:
where to decide ?
- userspace (The dwellstart phase does not look good in irq).This is agreed on.
- or irq ? NO!
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
- igncount and igndwellcount variables (not arrays, just 2 numbers; very efficient for the irq-routine)
- knowing current engphase
- and point of execution
However there might be benefits from having explicit ignition state (array):
- simplicity (easier to code and review)
- speed: simpler conditions for the processor: faster execution
The decoupled (non-independent) flags that are of interest:
- heap-state: lower 2 bits: what is actually scheduled (0/1/2 for nothing/dwellstart/spark as used in old code) for the given cyl.
- coil-state: is the coil actually activated/being charged? Might be useful at several places, but particularly important when early dwellstart is active, at the race between trigger and ignact (ignstate==4 or ignstate==3 in the old code).
Userspace suggestions or in-irq decisions that effect the actuation:
- dd2ds: should igndeact be scheduled from the dispatcher ignact action (using sparkat), or igndeact scheduled from a later tooth
- ds2dd: should ignact be scheduled from the dispatcher igndeact action (early dwellstart), or ignact scheduled from a later tooth
The new ignition-state concept (more spectacular than the old, but still only 5 states) is discussed in detail:\nÿ2ÿ
Limitations
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.
- rotary: with 24-tooth wheel, the trailing spark traditionally comes 15 crankdegrees after the leading spark. Consider: slight change in dwell or ignadv could cause collision and therefore a tooth-miss with the simple engphase_trig() implementation ? maybe the while is a good idea :-)
- Alpha twinspark: what is the desired crankangle (or time?) between the sparks? Are they simultaneous ?
When the state-machine is scetched, actuation is pie
We need a function to call at dwell_phase. Name dwell_trigger_here() is proposed. Very similar to ign_trigger_here(). In fact ign_trigger_here() could be split for reuse (static inline !). This (actuation part) is simple, but the userspace calculations for dwellstart are definitely more complex than for the spark events.
See also