boolignitionOn=false;//The current state of the ignition system
boolfuelOn=false;//The current state of the ignition system
boolfuelPumpOn=false;//The current status of the fuel pump
void(*trigger)();//Pointer for the trigger function (Gets pointed to the relevant decoder)
void(*triggerSecondary)();//Pointer for the secondary trigger function (Gets pointed to the relevant decoder)
int(*getRPM)();//Pointer to the getRPM function (Gets pointed to the relevant decoder)
int(*getCrankAngle)(int);//Pointer to the getCrank Angle function (Gets pointed to the relevant decoder)
bytecltCalibrationTable[CALIBRATION_TABLE_SIZE];
byteiatCalibrationTable[CALIBRATION_TABLE_SIZE];
byteo2CalibrationTable[CALIBRATION_TABLE_SIZE];
//These variables are used for tracking the number of running sensors values that appear to be errors. Once a threshold is reached, the sensor reading will go to default value and assume the sensor is faulty
bytemapErrorCount=0;
byteiatErrorCount=0;
bytecltErrorCount=0;
unsignedlongcounter;
unsignedlongcurrentLoopTime;//The time the current loop started (uS)
unsignedlongpreviousLoopTime;//The time the previous loop started (uS)
intCRANK_ANGLE_MAX=720;
intCRANK_ANGLE_MAX_IGN=360,CRANK_ANGLE_MAX_INJ=360;// The number of crank degrees that the system track over. 360 for wasted / timed batch and 720 for sequential
//bool useSequentialFuel; // Whether sequential fueling is to be used (1 squirt per cycle)
//bool useSequentialIgnition; // Whether sequential ignition is used (1 spark per cycle)
staticbytecoilHIGH=HIGH;
staticbytecoilLOW=LOW;
staticbytefanHIGH=HIGH;// Used to invert the cooling fan output
staticbytefanLOW=LOW;// Used to invert the cooling fan output
volatileintmainLoopCount;
bytedeltaToothCount=0;//The last tooth that was used with the deltaV calc
intrpmDelta;
byteignitionCount;
bytefixedCrankingOverride=0;
boolclutchTrigger;
boolpreviousClutchTrigger;
unsignedlongsecCounter;//The next time to incremen 'runSecs' counter.
intchannel1IgnDegrees;//The number of crank degrees until cylinder 1 is at TDC (This is obviously 0 for virtually ALL engines, but there's some weird ones)
intchannel2IgnDegrees;//The number of crank degrees until cylinder 2 (and 5/6/7/8) is at TDC
intchannel3IgnDegrees;//The number of crank degrees until cylinder 3 (and 5/6/7/8) is at TDC
intchannel4IgnDegrees;//The number of crank degrees until cylinder 4 (and 5/6/7/8) is at TDC
intchannel5IgnDegrees;//The number of crank degrees until cylinder 5 is at TDC
intchannel1InjDegrees;//The number of crank degrees until cylinder 1 is at TDC (This is obviously 0 for virtually ALL engines, but there's some weird ones)
intchannel2InjDegrees;//The number of crank degrees until cylinder 2 (and 5/6/7/8) is at TDC
intchannel3InjDegrees;//The number of crank degrees until cylinder 3 (and 5/6/7/8) is at TDC
intchannel4InjDegrees;//The number of crank degrees until cylinder 4 (and 5/6/7/8) is at TDC
intchannel5InjDegrees;//The number of crank degrees until cylinder 5 is at TDC
boolchannel1InjEnabled=true;
boolchannel2InjEnabled=false;
boolchannel3InjEnabled=false;
boolchannel4InjEnabled=false;
boolchannel5InjEnabled=false;
//These are the functions the get called to begin and end the ignition coil charging. They are required for the various spark output modes
void(*ign1StartFunction)();
void(*ign1EndFunction)();
void(*ign2StartFunction)();
void(*ign2EndFunction)();
void(*ign3StartFunction)();
void(*ign3EndFunction)();
void(*ign4StartFunction)();
void(*ign4EndFunction)();
void(*ign5StartFunction)();
void(*ign5EndFunction)();
inttimePerDegree;
bytedegreesPerLoop;//The number of crank degrees that pass for each mainloop of the program
volatileboolfpPrimed=false;//Tracks whether or not the fuel pump priming has been completed yet
//Once the configs have been loaded, a number of one time calculations can be completed
req_fuel_uS=configPage1.reqFuel*100;//Convert to uS and an int. This is the only variable to be used in calculations
inj_opentime_uS=configPage1.injOpen*100;//Injector open time. Comes through as ms*10 (Eg 15.5ms = 155).
//Begin the main crank trigger interrupt pin setup
//The interrupt numbering is a bit odd - See here for reference: http://arduino.cc/en/Reference/AttachInterrupt
//These assignments are based on the Arduino Mega AND VARY BETWEEN BOARDS. Please confirm the board you are using and update acordingly.
bytetriggerInterrupt=0;// By default, use the first interrupt
bytetriggerInterrupt2=1;
currentStatus.RPM=0;
currentStatus.hasSync=false;
currentStatus.runSecs=0;
currentStatus.secl=0;
currentStatus.startRevolutions=0;
currentStatus.flatShiftingHard=false;
currentStatus.launchingHard=false;
triggerFilterTime=0;//Trigger filter time is the shortest possible time (in uS) that there can be between crank teeth (ie at max RPM). Any pulses that occur faster than this time will be disgarded as noise. This is simply a default value, the actual values are set in the setup() functinos of each decoder
switch(pinTrigger){
//Arduino Mega 2560 mapping
case2:
triggerInterrupt=0;break;
case3:
triggerInterrupt=1;break;
case18:
triggerInterrupt=5;break;
case19:
triggerInterrupt=4;break;
case20:
triggerInterrupt=3;break;
case21:
triggerInterrupt=2;break;
}
switch(pinTrigger2){
//Arduino Mega 2560 mapping
case2:
triggerInterrupt2=0;break;
case3:
triggerInterrupt2=1;break;
case18:
triggerInterrupt2=5;break;
case19:
triggerInterrupt2=4;break;
case20:
triggerInterrupt2=3;break;
case21:
triggerInterrupt2=2;break;
}
pinMode(pinTrigger,INPUT);
pinMode(pinTrigger2,INPUT);
pinMode(pinTrigger3,INPUT);
//digitalWrite(pinTrigger, HIGH);
//Set the trigger function based on the decoder in the config
switch(configPage2.TrigPattern)
{
case0:
//Missing tooth decoder
triggerSetup_missingTooth();
trigger=triggerPri_missingTooth;
triggerSecondary=triggerSec_missingTooth;
getRPM=getRPM_missingTooth;
getCrankAngle=getCrankAngle_missingTooth;
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
else{attachInterrupt(triggerInterrupt,trigger,FALLING);}// Primary trigger connects to
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
else{attachInterrupt(triggerInterrupt,trigger,FALLING);}// Primary trigger connects to
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
else{attachInterrupt(triggerInterrupt,trigger,FALLING);}// Primary trigger connects to
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
else{attachInterrupt(triggerInterrupt,trigger,FALLING);}// Primary trigger connects to
trigger=triggerPri_DualWheel;//Is identical to the dual wheel decoder, so that is used. Same goes for the secondary below
getRPM=getRPM_non360;
getCrankAngle=getCrankAngle_non360;
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
attachInterrupt(triggerInterrupt2,triggerSec_DualWheel,FALLING);//Note the use of the Dual Wheel trigger function here. No point in having the same code in twice.
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
if(configPage2.TrigEdge==0){attachInterrupt(triggerInterrupt,trigger,RISING);}// Attach the crank trigger wheel interrupt (Hall sensor drags to ground when triggering)
req_fuel_uS=req_fuel_uS/engineSquirtsPerCycle;//The req_fuel calculation above gives the total required fuel (At VE 100%) in the full cycle. If we're doing more than 1 squirt per cycle then we need to split the amount accordingly. (Note that in a non-sequential 4-stroke setup you cannot have less than 2 squirts as you cannot determine the stroke to make the single squirt on)
//Initial values for loop times
previousLoopTime=0;
currentLoopTime=micros();
mainLoopCount=0;
ignitionCount=0;
//Calculate the number of degrees between cylinders
switch(configPage1.nCylinders){
case1:
channel1IgnDegrees=0;
channel1InjDegrees=0;
channel1InjEnabled=true;
break;
case2:
channel1IgnDegrees=0;
if(configPage1.engineType==EVEN_FIRE)
{
channel2IgnDegrees=180;
}
else{channel2IgnDegrees=configPage1.oddfire2;}
//For alternating injection, the squirt occurs at different times for each channel
if(configPage1.injLayout==INJ_SEMISEQUENTIAL)
{
channel1InjDegrees=0;
channel2InjDegrees=channel2IgnDegrees;//Set to the same as the ignition degrees (Means there's no need for another if to check for oddfire)
}
if(!configPage1.injTiming){channel1InjDegrees=channel2InjDegrees=0;}//For simultaneous, all squirts happen at the same time
channel1InjEnabled=true;
channel2InjEnabled=true;
break;
case3:
channel1IgnDegrees=0;
if(configPage1.engineType==EVEN_FIRE)
{
channel2IgnDegrees=120;
channel3IgnDegrees=240;
}
else
{
channel2IgnDegrees=configPage1.oddfire2;
channel3IgnDegrees=configPage1.oddfire3;
}
//For alternatiing injection, the squirt occurs at different times for each channel
if(!configPage1.injTiming){channel1InjDegrees=channel2InjDegrees=channel3InjDegrees=channel4InjDegrees=channel5InjDegrees=0;}//For simultaneous, all squirts happen at the same time
channel1InjEnabled=true;
channel2InjEnabled=true;
channel3InjEnabled=false;//this is disabled as injector 5 function calls 3 & 5 together
channel4InjEnabled=true;
channel5InjEnabled=true;
break;
case6:
channel1IgnDegrees=0;
channel2IgnDegrees=120;
channel3IgnDegrees=240;
//For alternatiing injection, the squirt occurs at different times for each channel
/*
if(configPage1.injLayout==INJ_SEMISEQUENTIAL||configPage1.injLayout==INJ_SEQUENTIAL||configPage1.injLayout==INJ_PAIRED)//No full sequential for more than 4 cylinders
{
channel1InjDegrees=0;
channel2InjDegrees=120;
channel3InjDegrees=240;
}
*/
if(!configPage1.injTiming){channel1InjDegrees=channel2InjDegrees=channel3InjDegrees=0;}//For simultaneous, all squirts happen at the same time
configPage1.injLayout=0;//This is a failsafe. We can never run semi-sequential with more than 4 cylinders
channel1InjEnabled=true;
channel2InjEnabled=true;
channel3InjEnabled=true;
break;
case8:
channel1IgnDegrees=0;
channel2IgnDegrees=90;
channel3IgnDegrees=180;
channel4IgnDegrees=270;
//For alternatiing injection, the squirt occurs at different times for each channel
/*
if(configPage1.injLayout==INJ_SEMISEQUENTIAL||configPage1.injTiming==INJ_SEQUENTIAL)//No full sequential for more than 4 cylinders
{
channel1InjDegrees=0;
channel2InjDegrees=90;
channel3InjDegrees=180;
channel4InjDegrees=270;
}
*/
if(!configPage1.injTiming){channel1InjDegrees=channel2InjDegrees=channel3InjDegrees=channel4InjDegrees=0;}//For simultaneous, all squirts happen at the same time
configPage1.injLayout=0;//This is a failsafe. We can never run semi-sequential with more than 4 cylinders
channel1InjEnabled=true;
channel2InjEnabled=true;
channel3InjEnabled=true;
channel4InjEnabled=true;
break;
default://Handle this better!!!
channel1InjDegrees=0;
channel2InjDegrees=180;
break;
}
switch(configPage2.sparkMode)
{
caseIGN_MODE_WASTED:
//Wasted Spark (Normal mode)
ign1StartFunction=beginCoil1Charge;
ign1EndFunction=endCoil1Charge;
ign2StartFunction=beginCoil2Charge;
ign2EndFunction=endCoil2Charge;
ign3StartFunction=beginCoil3Charge;
ign3EndFunction=endCoil3Charge;
ign4StartFunction=beginCoil4Charge;
ign4EndFunction=endCoil4Charge;
ign5StartFunction=beginCoil5Charge;
ign5EndFunction=endCoil5Charge;
break;
caseIGN_MODE_SINGLE:
//Single channel mode. All ignition pulses are on channel 1
ign1StartFunction=beginCoil1Charge;
ign1EndFunction=endCoil1Charge;
ign2StartFunction=beginCoil1Charge;
ign2EndFunction=endCoil1Charge;
ign3StartFunction=beginCoil1Charge;
ign3EndFunction=endCoil1Charge;
ign4StartFunction=beginCoil1Charge;
ign4EndFunction=endCoil1Charge;
ign5StartFunction=beginCoil1Charge;
ign5EndFunction=endCoil1Charge;
break;
caseIGN_MODE_WASTEDCOP:
//Wasted COP mode. Ignition channels 1&3 and 2&4 are paired together
//This is not a valid mode for >4 cylinders
if(configPage1.nCylinders<=4)
{
ign1StartFunction=beginCoil1and3Charge;
ign1EndFunction=endCoil1and3Charge;
ign2StartFunction=beginCoil2and4Charge;
ign2EndFunction=endCoil2and4Charge;
ign3StartFunction=nullCallback;
ign3EndFunction=nullCallback;
ign4StartFunction=nullCallback;
ign4EndFunction=nullCallback;
}
else
{
//If the person has inadvertantly selected this when running more than 4 cylinders, just use standard Wasted spark mode
ign1StartFunction=beginCoil1Charge;
ign1EndFunction=endCoil1Charge;
ign2StartFunction=beginCoil2Charge;
ign2EndFunction=endCoil2Charge;
ign3StartFunction=beginCoil3Charge;
ign3EndFunction=endCoil3Charge;
ign4StartFunction=beginCoil4Charge;
ign4EndFunction=endCoil4Charge;
ign5StartFunction=beginCoil5Charge;
ign5EndFunction=endCoil5Charge;
}
break;
caseIGN_MODE_SEQUENTIAL:
ign1StartFunction=beginCoil1Charge;
ign1EndFunction=endCoil1Charge;
ign2StartFunction=beginCoil2Charge;
ign2EndFunction=endCoil2Charge;
ign3StartFunction=beginCoil3Charge;
ign3EndFunction=endCoil3Charge;
ign4StartFunction=beginCoil4Charge;
ign4EndFunction=endCoil4Charge;
ign5StartFunction=beginCoil5Charge;
ign5EndFunction=endCoil5Charge;
break;
default:
//Wasted spark (Shouldn't ever happen anyway)
ign1StartFunction=beginCoil1Charge;
ign1EndFunction=endCoil1Charge;
ign2StartFunction=beginCoil2Charge;
ign2EndFunction=endCoil2Charge;
ign3StartFunction=beginCoil3Charge;
ign3EndFunction=endCoil3Charge;
ign4StartFunction=beginCoil4Charge;
ign4EndFunction=endCoil4Charge;
ign5StartFunction=beginCoil5Charge;
ign5EndFunction=endCoil5Charge;
break;
}
//Begin priming the fuel pump. This is turned off in the low resolution, 1s interrupt in timers.ino
digitalWrite(pinFuelPump,HIGH);
fuelPumpOn=true;
//Perform the priming pulses. Set these to run at an arbitrary time in the future (100us). The prime pulse value is in ms*10, so need to multiple by 100 to get to uS
//Check for any requets from serial. Serial operations are checked under 2 scenarios:
// 1) Every 64 loops (64 Is more than fast enough for TunerStudio). This function is equivalent to ((loopCount % 64) == 1) but is considerably faster due to not using the mod or division operations
// 2) If the amount of data in the serial buffer is greater than a set threhold (See globals.h). This is to avoid serial buffer overflow when large amounts of data is being sent
if((timeToLastTooth<MAX_STALL_TIME)||(toothLastToothTime>currentLoopTime))//Check how long ago the last tooth was seen compared to now. If it was more than half a second ago then the engine is probably stopped. toothLastToothTime can be greater than currentLoopTime if a pulse occurs between getting the lastest time and doing the comparison
{
currentStatus.RPM=currentStatus.longRPM=getRPM();//Long RPM is included here
if(fuelPumpOn==false){digitalWrite(pinFuelPump,HIGH);fuelPumpOn=true;}//Check if the fuel pump is on and turn it on if it isn't.
}
else
{
//We reach here if the time between teeth is too great. This VERY likely means the engine has stopped
currentStatus.RPM=0;
currentStatus.PW1=0;
currentStatus.VE=0;
toothLastToothTime=0;
currentStatus.hasSync=false;
currentStatus.runSecs=0;//Reset the counter for number of seconds running.
if(configPage4.iacAlgorithm==IAC_ALGORITHM_STEP_OL||configPage4.iacAlgorithm==IAC_ALGORITHM_STEP_CL){idleControl();}//Run idlecontrol every loop for stepper idle.
if(currentStatus.startRevolutions>=configPage2.StgCycles){ignitionOn=true;fuelOn=true;}//Enable the fuel and ignition, assuming staging revolutions are complete
//If it is, check is we're running or cranking
if(currentStatus.RPM>((unsignedint)configPage2.crankRPM*100))//Crank RPM stored in byte as RPM / 100
{
BIT_SET(currentStatus.engine,BIT_ENGINE_RUN);//Sets the engine running bit
//Only need to do anything if we're transitioning from cranking to running
currentStatus.advance=get3DTableValue(&ignitionTable,currentStatus.MAP,currentStatus.RPM);//As above, but for ignition advance
}
else
{
//Alpha-N
currentStatus.VE=get3DTableValue(&fuelTable,currentStatus.TPS,currentStatus.RPM);//Perform lookup into fuel map for RPM vs TPS value
currentStatus.PW1=PW_AN(req_fuel_uS,currentStatus.VE,currentStatus.TPS,currentStatus.corrections,inj_opentime_uS);//Calculate pulsewidth using the Alpha-N algorithm (in uS)
currentStatus.advance=get3DTableValue(&ignitionTable,currentStatus.TPS,currentStatus.RPM);//As above, but for ignition advance
//How fast are we going? Need to know how long (uS) it will take to get from one tooth to the next. We then use that to estimate how far we are between the last tooth and the next one
//We use a 1st Deriv accleration prediction, but only when there is an even spacing between primary sensor teeth
//Any decoder that has uneven spacing has its triggerToothAngle set to 0
if(secondDerivEnabled&&toothHistoryIndex>=3&¤tStatus.RPM<2000)//toothHistoryIndex must be greater than or equal to 3 as we need the last 3 entries. Currently this mode only runs below 3000 rpm
//if(true)
{
//Only recalculate deltaV if the tooth has changed since last time (DeltaV stays the same until the next tooth)
//if (deltaToothCount != toothCurrentCount)
{
deltaToothCount=toothCurrentCount;
intangle1,angle2;//These represent the crank angles that are travelled for the last 2 pulses
if(configPage2.TrigPattern==4)
{
//Special case for 70/110 pattern on 4g63
angle2=triggerToothAngle;//Angle 2 is the most recent
if(angle2==70){angle1=110;}
else{angle1=70;}
}
elseif(configPage2.TrigPattern==0)
{
//Special case for missing tooth decoder where the missing tooth was one of the last 2 seen
//long timeToLastTooth = micros() - toothLastToothTime; //Cannot be unsigned
rpmDelta=(toothDeltaV<<10)/(6*toothDeltaT);
}
timePerDegree=ldiv(166666L,(currentStatus.RPM+rpmDelta)).quot;//There is a small amount of rounding in this calculation, however it is less than 0.001 of a uS (Faster as ldiv than / )
}
else
{
longrpm_adjust=((long)(micros()-toothOneTime)*(long)currentStatus.rpmDOT)/1000000;//Take into account any likely accleration that has occurred since the last full revolution completed
timePerDegree=ldiv(166666L,currentStatus.RPM+rpm_adjust).quot;//There is a small amount of rounding in this calculation, however it is less than 0.001 of a uS (Faster as ldiv than / )
}
//Check that the duty cycle of the chosen pulsewidth isn't too high. This is disabled at cranking
unsignedlongpwLimit=percentage(configPage1.dutyLim,revolutionTime);//The pulsewidth limit is determined to be the duty cycle limit (Eg 85%) by the total time it takes to perform 1 revolution
if(CRANK_ANGLE_MAX_INJ==720){pwLimit=pwLimit*2;}//For sequential, the maximum pulse time is double (2 revolutions). Wouldn't work for 2 stroke...
currentStatus.PW2=currentStatus.PW3=currentStatus.PW4=currentStatus.PW1;// Initial state is for all pulsewidths to be the same (This gets changed below)
if(!configPage1.indInjAng){configPage1.inj4Ang=configPage1.inj3Ang=configPage1.inj2Ang=configPage1.inj1Ang;}//Forcing all injector close angles to be the same.
intPWdivTimerPerDegree=div(currentStatus.PW1,timePerDegree).quot;//How many crank degrees the calculated PW will take at the current speed
injector1StartAngle=configPage1.inj1Ang-(PWdivTimerPerDegree);//This is a little primitive, but is based on the idea that all fuel needs to be delivered before the inlet valve opens. See http://www.extraefi.co.uk/sequential_fuel.html for more detail
//fixedCrankingOverride is used to extend the dwell during cranking so that the decoder can trigger the spark upon seeing a certain tooth. Currently only available on the basic distributor and 4g63 decoders.