Initial work on high res internal timing

This commit is contained in:
Josh Stewart 2017-04-07 00:23:47 +10:00
parent fa83faa2f0
commit 2eb8e8e91f
3 changed files with 266 additions and 263 deletions

File diff suppressed because it is too large Load Diff

View File

@ -88,14 +88,14 @@ Note: This does not currently support dual wheel (ie missing tooth + single toot
*/ */
void triggerSetup_missingTooth() void triggerSetup_missingTooth()
{ {
triggerToothAngle = 360 / configPage2.triggerTeeth; //The number of degrees that passes from tooth to tooth triggerToothAngle = 3600 / configPage2.triggerTeeth; //The number of degrees that passes from tooth to tooth
if(configPage2.TrigSpeed) { triggerToothAngle = 720 / configPage2.triggerTeeth; } //Account for cam speed missing tooth if(configPage2.TrigSpeed) { triggerToothAngle = 7200 / configPage2.triggerTeeth; } //Account for cam speed missing tooth
triggerActualTeeth = configPage2.triggerTeeth - configPage2.triggerMissingTeeth; //The number of physical teeth on the wheel. Doing this here saves us a calculation each time in the interrupt triggerActualTeeth = configPage2.triggerTeeth - configPage2.triggerMissingTeeth; //The number of physical teeth on the wheel. Doing this here saves us a calculation each time in the interrupt
triggerFilterTime = (int)(1000000 / (MAX_RPM / 60 * configPage2.triggerTeeth)); //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 triggerFilterTime = (int)(1000000 / (MAX_RPM / 60 * configPage2.triggerTeeth)); //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
secondDerivEnabled = false; secondDerivEnabled = false;
decoderIsSequential = false; decoderIsSequential = false;
checkSyncToothCount = (configPage2.triggerTeeth) >> 1; //50% of the total teeth. checkSyncToothCount = (configPage2.triggerTeeth) >> 1; //50% of the total teeth.
MAX_STALL_TIME = (3333UL * triggerToothAngle * (configPage2.triggerMissingTeeth + 1)); //Minimum 50rpm. (3333uS is the time per degree at 50rpm) MAX_STALL_TIME = (3333UL * (triggerToothAngle / 10) * (configPage2.triggerMissingTeeth + 1)); //Minimum 50rpm. (3333uS is the time per degree at 50rpm)
} }
void triggerPri_missingTooth() void triggerPri_missingTooth()
@ -164,7 +164,7 @@ int getCrankAngle_missingTooth(int timePerDegree)
tempRevolutionOne = revolutionOne; tempRevolutionOne = revolutionOne;
interrupts(); interrupts();
int crankAngle = (tempToothCurrentCount - 1) * triggerToothAngle + configPage2.triggerAngle; //Number of teeth that have passed since tooth 1, multiplied by the angle each tooth represents, plus the angle that tooth 1 is ATDC. This gives accuracy only to the nearest tooth. int crankAngle = (tempToothCurrentCount - 1) * triggerToothAngle + (configPage2.triggerAngle * 10); //Number of teeth that have passed since tooth 1, multiplied by the angle each tooth represents, plus the angle that tooth 1 is ATDC. This gives accuracy only to the nearest tooth.
//Estimate the number of degrees travelled since the last tooth} //Estimate the number of degrees travelled since the last tooth}
long elapsedTime = (micros() - tempToothLastToothTime); long elapsedTime = (micros() - tempToothLastToothTime);
//crankAngle += DIV_ROUND_CLOSEST(elapsedTime, timePerDegree); //crankAngle += DIV_ROUND_CLOSEST(elapsedTime, timePerDegree);
@ -172,9 +172,9 @@ int getCrankAngle_missingTooth(int timePerDegree)
else { crankAngle += ldiv(elapsedTime, timePerDegree).quot; } else { crankAngle += ldiv(elapsedTime, timePerDegree).quot; }
//Sequential check (simply sets whether we're on the first or 2nd revoltuion of the cycle) //Sequential check (simply sets whether we're on the first or 2nd revoltuion of the cycle)
if (tempRevolutionOne) { crankAngle += 360; } if (tempRevolutionOne) { crankAngle += 3600; }
if (crankAngle >= 720) { crankAngle -= 720; } if (crankAngle >= 7200) { crankAngle -= 7200; }
else if (crankAngle > CRANK_ANGLE_MAX) { crankAngle -= CRANK_ANGLE_MAX; } else if (crankAngle > CRANK_ANGLE_MAX) { crankAngle -= CRANK_ANGLE_MAX; }
if (crankAngle < 0) { crankAngle += CRANK_ANGLE_MAX; } if (crankAngle < 0) { crankAngle += CRANK_ANGLE_MAX; }
@ -1545,7 +1545,7 @@ void triggerSetup_Subaru67()
secondDerivEnabled = false; secondDerivEnabled = false;
decoderIsSequential = true; decoderIsSequential = true;
toothCurrentCount = 1; toothCurrentCount = 1;
triggerToothAngle = 2; //triggerToothAngle = 2;
MAX_STALL_TIME = (3333UL * 93); //Minimum 50rpm. (3333uS is the time per degree at 50rpm) MAX_STALL_TIME = (3333UL * 93); //Minimum 50rpm. (3333uS is the time per degree at 50rpm)
toothAngles[0] = 710; //tooth #1 toothAngles[0] = 710; //tooth #1

View File

@ -78,8 +78,8 @@ unsigned long counter;
unsigned long currentLoopTime; //The time the current loop started (uS) unsigned long currentLoopTime; //The time the current loop started (uS)
unsigned long previousLoopTime; //The time the previous loop started (uS) unsigned long previousLoopTime; //The time the previous loop started (uS)
int CRANK_ANGLE_MAX = 720; int CRANK_ANGLE_MAX = 7200;
int CRANK_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 int CRANK_ANGLE_MAX_IGN = 3600, CRANK_ANGLE_MAX_INJ = 3600; // 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 useSequentialFuel; // Whether sequential fueling is to be used (1 squirt per cycle)
//bool useSequentialIgnition; // Whether sequential ignition is used (1 spark per cycle) //bool useSequentialIgnition; // Whether sequential ignition is used (1 spark per cycle)
@ -126,7 +126,8 @@ void (*ign4EndFunction)();
void (*ign5StartFunction)(); void (*ign5StartFunction)();
void (*ign5EndFunction)(); void (*ign5EndFunction)();
int timePerDegree; unsigned int timePerDegree;
unsigned int timePer10Degree;
byte degreesPerLoop; //The number of crank degrees that pass for each mainloop of the program byte degreesPerLoop; //The number of crank degrees that pass for each mainloop of the program
volatile bool fpPrimed = false; //Tracks whether or not the fuel pump priming has been completed yet volatile bool fpPrimed = false; //Tracks whether or not the fuel pump priming has been completed yet
@ -514,9 +515,9 @@ void setup()
if (configPage1.engineType == EVEN_FIRE ) if (configPage1.engineType == EVEN_FIRE )
{ {
channel2IgnDegrees = 180; channel2IgnDegrees = 1800;
} }
else { channel2IgnDegrees = configPage1.oddfire2; } else { channel2IgnDegrees = configPage1.oddfire2 * 10; }
//For alternating injection, the squirt occurs at different times for each channel //For alternating injection, the squirt occurs at different times for each channel
if(configPage1.injLayout == INJ_SEMISEQUENTIAL) if(configPage1.injLayout == INJ_SEMISEQUENTIAL)
@ -534,13 +535,13 @@ void setup()
if (configPage1.engineType == EVEN_FIRE ) if (configPage1.engineType == EVEN_FIRE )
{ {
channel2IgnDegrees = 120; channel2IgnDegrees = 1200;
channel3IgnDegrees = 240; channel3IgnDegrees = 2400;
} }
else else
{ {
channel2IgnDegrees = configPage1.oddfire2; channel2IgnDegrees = configPage1.oddfire2 * 10;
channel3IgnDegrees = configPage1.oddfire3; channel3IgnDegrees = configPage1.oddfire3 * 10;
} }
//For alternatiing injection, the squirt occurs at different times for each channel //For alternatiing injection, the squirt occurs at different times for each channel
@ -553,9 +554,9 @@ void setup()
else if (configPage1.injLayout == INJ_SEQUENTIAL) else if (configPage1.injLayout == INJ_SEQUENTIAL)
{ {
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 240; channel2InjDegrees = 2400;
channel3InjDegrees = 480; channel3InjDegrees = 4800;
CRANK_ANGLE_MAX_INJ = 720; CRANK_ANGLE_MAX_INJ = 7200;
req_fuel_uS = req_fuel_uS * 2; req_fuel_uS = req_fuel_uS * 2;
} }
if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = 0; } //For simultaneous, all squirts happen at the same time if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = 0; } //For simultaneous, all squirts happen at the same time
@ -569,40 +570,40 @@ void setup()
if (configPage1.engineType == EVEN_FIRE ) if (configPage1.engineType == EVEN_FIRE )
{ {
channel2IgnDegrees = 180; channel2IgnDegrees = 1800;
if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL) if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL)
{ {
channel3IgnDegrees = 360; channel3IgnDegrees = 3600;
channel4IgnDegrees = 540; channel4IgnDegrees = 5400;
CRANK_ANGLE_MAX_IGN = 720; CRANK_ANGLE_MAX_IGN = 7200;
} }
} }
else else
{ {
channel2IgnDegrees = configPage1.oddfire2; channel2IgnDegrees = configPage1.oddfire2 * 10;
channel3IgnDegrees = configPage1.oddfire3; channel3IgnDegrees = configPage1.oddfire3 * 10;
channel4IgnDegrees = configPage1.oddfire4; channel4IgnDegrees = configPage1.oddfire4 * 10;
} }
//For alternatiing injection, the squirt occurs at different times for each channel //For alternatiing injection, the squirt occurs at different times for each channel
if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injLayout == INJ_PAIRED) if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injLayout == INJ_PAIRED)
{ {
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 180; channel2InjDegrees = 1800;
} }
else if (configPage1.injLayout == INJ_SEQUENTIAL) else if (configPage1.injLayout == INJ_SEQUENTIAL)
{ {
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 180; channel2InjDegrees = 1800;
channel3InjDegrees = 360; channel3InjDegrees = 3600;
channel4InjDegrees = 540; channel4InjDegrees = 5400;
channel3InjEnabled = true; channel3InjEnabled = true;
channel4InjEnabled = true; channel4InjEnabled = true;
CRANK_ANGLE_MAX_INJ = 720; CRANK_ANGLE_MAX_INJ = 7200;
req_fuel_uS = req_fuel_uS * 2; req_fuel_uS = req_fuel_uS * 2;
} }
if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = 0; } //For simultaneous, all squirts happen at the same time if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = 0; } //For simultaneous, all squirts happen at the same time
@ -612,39 +613,39 @@ void setup()
break; break;
case 5: case 5:
channel1IgnDegrees = 0; channel1IgnDegrees = 0;
channel2IgnDegrees = 72; channel2IgnDegrees = 720;
channel3IgnDegrees = 144; channel3IgnDegrees = 1440;
channel4IgnDegrees = 216; channel4IgnDegrees = 2160;
channel5IgnDegrees = 288; channel5IgnDegrees = 2880;
if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL) if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL)
{ {
channel2IgnDegrees = 144; channel2IgnDegrees = 1440;
channel3IgnDegrees = 288; channel3IgnDegrees = 2880;
channel4IgnDegrees = 432; channel4IgnDegrees = 4320;
channel5IgnDegrees = 576; channel5IgnDegrees = 5760;
CRANK_ANGLE_MAX_IGN = 720; CRANK_ANGLE_MAX_IGN = 7200;
} }
//For alternatiing injection, the squirt occurs at different times for each channel //For alternatiing injection, the squirt occurs at different times for each channel
if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injLayout == INJ_PAIRED) if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injLayout == INJ_PAIRED)
{ {
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 72; channel2InjDegrees = 720;
channel3InjDegrees = 144; channel3InjDegrees = 1440;
channel4InjDegrees = 216; channel4InjDegrees = 2160;
channel5InjDegrees = 288; channel5InjDegrees = 2880;
} }
else if (configPage1.injLayout == INJ_SEQUENTIAL) else if (configPage1.injLayout == INJ_SEQUENTIAL)
{ {
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 144; channel2InjDegrees = 1440;
channel3InjDegrees = 288; channel3InjDegrees = 2880;
channel4InjDegrees = 432; channel4InjDegrees = 4320;
channel5InjDegrees = 576; channel5InjDegrees = 5760;
CRANK_ANGLE_MAX_INJ = 720; CRANK_ANGLE_MAX_INJ = 7200;
} }
if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = channel4InjDegrees = channel5InjDegrees = 0; } //For simultaneous, all squirts happen at the same time if (!configPage1.injTiming) { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = channel4InjDegrees = channel5InjDegrees = 0; } //For simultaneous, all squirts happen at the same time
@ -656,8 +657,8 @@ void setup()
break; break;
case 6: case 6:
channel1IgnDegrees = 0; channel1IgnDegrees = 0;
channel2IgnDegrees = 120; channel2IgnDegrees = 1200;
channel3IgnDegrees = 240; channel3IgnDegrees = 2400;
//For alternatiing injection, the squirt occurs at different times for each channel //For alternatiing injection, the squirt occurs at different times for each channel
/* /*
@ -678,9 +679,9 @@ void setup()
break; break;
case 8: case 8:
channel1IgnDegrees = 0; channel1IgnDegrees = 0;
channel2IgnDegrees = 90; channel2IgnDegrees = 900;
channel3IgnDegrees = 180; channel3IgnDegrees = 1800;
channel4IgnDegrees = 270; channel4IgnDegrees = 2700;
//For alternatiing injection, the squirt occurs at different times for each channel //For alternatiing injection, the squirt occurs at different times for each channel
/* /*
@ -703,7 +704,7 @@ void setup()
break; break;
default: //Handle this better!!! default: //Handle this better!!!
channel1InjDegrees = 0; channel1InjDegrees = 0;
channel2InjDegrees = 180; channel2InjDegrees = 1800;
break; break;
} }
@ -979,7 +980,7 @@ void loop()
currentStatus.advance = get3DTableValue(&ignitionTable, currentStatus.TPS, currentStatus.RPM); //As above, but for ignition advance currentStatus.advance = get3DTableValue(&ignitionTable, currentStatus.TPS, currentStatus.RPM); //As above, but for ignition advance
} }
currentStatus.advance = correctionsIgn(currentStatus.advance); currentStatus.advance = correctionsIgn(currentStatus.advance) * 10;
/* /*
//Check for fixed ignition angles //Check for fixed ignition angles
if (configPage2.FixAng != 0) { currentStatus.advance = configPage2.FixAng; } //Check whether the user has set a fixed timing angle if (configPage2.FixAng != 0) { currentStatus.advance = configPage2.FixAng; } //Check whether the user has set a fixed timing angle
@ -1041,21 +1042,23 @@ void loop()
} }
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 / ) timePer10Degree = ldiv( 1666666L, (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 / )
timePerDegree = timePer10Degree / 10;
} }
else else
{ {
long rpm_adjust = ((long)(micros() - toothOneTime) * (long)currentStatus.rpmDOT) / 1000000; //Take into account any likely accleration that has occurred since the last full revolution completed long rpm_adjust = ((long)(micros() - toothOneTime) * (long)currentStatus.rpmDOT) / 1000000; //Take into account any likely accleration that has occurred since the last full revolution completed
//timePerDegree = DIV_ROUND_CLOSEST(166666L, (currentStatus.RPM + rpm_adjust)); //timePerDegree = DIV_ROUND_CLOSEST(166666L, (currentStatus.RPM + rpm_adjust));
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 / ) timePer10Degree = ldiv( 1666666L, 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 / )
timePerDegree = timePer10Degree / 10;
} }
//Check that the duty cycle of the chosen pulsewidth isn't too high. This is disabled at cranking //Check that the duty cycle of the chosen pulsewidth isn't too high. This is disabled at cranking
if( !BIT_CHECK(currentStatus.engine, BIT_ENGINE_CRANK) ) if( !BIT_CHECK(currentStatus.engine, BIT_ENGINE_CRANK) )
{ {
unsigned long pwLimit = 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 unsigned long pwLimit = 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... if (CRANK_ANGLE_MAX_INJ == 7200) { pwLimit = pwLimit * 2; } //For sequential, the maximum pulse time is double (2 revolutions). Wouldn't work for 2 stroke...
if (currentStatus.PW1 > pwLimit) { currentStatus.PW1 = pwLimit; } if (currentStatus.PW1 > pwLimit) { currentStatus.PW1 = pwLimit; }
} }
@ -1155,7 +1158,7 @@ void loop()
//Pull battery voltage based dwell correction and apply if needed //Pull battery voltage based dwell correction and apply if needed
currentStatus.dwellCorrection = table2D_getValue(&dwellVCorrectionTable, currentStatus.battery10); currentStatus.dwellCorrection = table2D_getValue(&dwellVCorrectionTable, currentStatus.battery10);
if (currentStatus.dwellCorrection != 100) { currentStatus.dwell = divs100(currentStatus.dwell) * currentStatus.dwellCorrection; } if (currentStatus.dwellCorrection != 100) { currentStatus.dwell = divs100(currentStatus.dwell) * currentStatus.dwellCorrection; }
int dwellAngle = (div(currentStatus.dwell, timePerDegree).quot ); //Convert the dwell time to dwell angle based on the current engine speed unsigned int dwellAngle = (div((currentStatus.dwell*10), timePer10Degree).quot ); //Convert the dwell time to dwell angle based on the current engine speed
//Calculate start angle for each channel //Calculate start angle for each channel
//1 cylinder (Everyone gets this) //1 cylinder (Everyone gets this)
@ -1174,7 +1177,7 @@ void loop()
case 3: case 3:
ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle;
if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;}
ignition3StartAngle = channel3IgnDegrees + 360 - currentStatus.advance - dwellAngle; ignition3StartAngle = channel3IgnDegrees + 3600 - currentStatus.advance - dwellAngle;
if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;} if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;}
break; break;
//4 cylinders //4 cylinders
@ -1237,7 +1240,7 @@ void loop()
//Determine the current crank angle //Determine the current crank angle
int crankAngle = getCrankAngle(timePerDegree); int crankAngle = getCrankAngle(timePerDegree);
if (crankAngle > CRANK_ANGLE_MAX_INJ ) { crankAngle -= 360; } if (crankAngle > CRANK_ANGLE_MAX_INJ ) { crankAngle -= 3600; }
if (fuelOn && currentStatus.PW1 > 0 && !BIT_CHECK(currentStatus.squirt, BIT_SQUIRT_BOOSTCUT)) if (fuelOn && currentStatus.PW1 > 0 && !BIT_CHECK(currentStatus.squirt, BIT_SQUIRT_BOOSTCUT))
{ {
@ -1369,7 +1372,7 @@ void loop()
//Refresh the current crank angle info //Refresh the current crank angle info
//ignition1StartAngle = 335; //ignition1StartAngle = 335;
crankAngle = getCrankAngle(timePerDegree); //Refresh with the latest crank angle crankAngle = getCrankAngle(timePerDegree); //Refresh with the latest crank angle
if (crankAngle > CRANK_ANGLE_MAX_IGN ) { crankAngle -= 360; } if (crankAngle > CRANK_ANGLE_MAX_IGN ) { crankAngle -= 3600; }
//if (ignition1StartAngle <= crankAngle && ignition1.schedulesSet == 0) { ignition1StartAngle += CRANK_ANGLE_MAX_IGN; } //if (ignition1StartAngle <= crankAngle && ignition1.schedulesSet == 0) { ignition1StartAngle += CRANK_ANGLE_MAX_IGN; }
if (ignition1StartAngle > crankAngle) if (ignition1StartAngle > crankAngle)
@ -1382,7 +1385,7 @@ void loop()
unsigned long timeout = (unsigned long)(ignition1StartAngle - crankAngle) * 282UL; unsigned long timeout = (unsigned long)(ignition1StartAngle - crankAngle) * 282UL;
*/ */
setIgnitionSchedule1(ign1StartFunction, setIgnitionSchedule1(ign1StartFunction,
((unsigned long)(ignition1StartAngle - crankAngle) * (unsigned long)timePerDegree), //(timeout/10), ((unsigned long)(ignition1StartAngle - crankAngle) * (unsigned long)timePer10Degree)/100, //(timeout/10),
currentStatus.dwell + fixedCrankingOverride, //((unsigned long)((unsigned long)currentStatus.dwell* currentStatus.RPM) / newRPM) + fixedCrankingOverride, currentStatus.dwell + fixedCrankingOverride, //((unsigned long)((unsigned long)currentStatus.dwell* currentStatus.RPM) / newRPM) + fixedCrankingOverride,
ign1EndFunction ign1EndFunction
); );