// 4 stroke engine control system // // !!!! UNDER CONSTRUCTION !!! // // Author: Arend Lammertink // Date latest revision: 3 January 2014 // See: http://www.tuks.nl/wiki/index.php/Main/Arduino // // Based on: // // Frequency timer using input capture unit // Author: Nick Gammon // Date: 31 August 2013 // See: http://www.gammon.com.au/forum/?id=11504 // /* 4 stroke engine cycle. We begin at BDC, when the outlet opens, the Hall sensor is connected to the outlet and all calculations are based on the rising edge of the Hall sensor. degrees action ------- ------ 0 exhaust valve open 180 exhaust valve close 180 inlet valve open (TDC) 360 inlet valve close 360 - 540 compression (TDC) 550 ignition (HHO, 10 degrees after TDC) 550 - 720 work Gas injection timing -------------------- Les Banki gives some detailed information about his gas injector timing for his analog engine control system: http://www.tuks.nl/wiki/index.php/Main/LesBankiProject#IgnitionInjectionControlModule -:- Injection In the general practice of the "art", the injection solenoid is sometimes turned on just before the intake valve opens to gain a little extra time for injection. (But it must be turned off before the valve closes.) The volume of gas injected is determined by gas pressure AND the solenoid's ON-time. Just as with the ignition, injection timing is related to the engine's work cycle. The injection circuit is almost identical to the ignition circuit with a couple of differences. Just like the ignition circuit, the injection comparator (IC9B-LM393-2) gets its linear saw tooth signal from the output of IC4A (pin 1 & TP9), connected through R30 (10k), to its inverting (-) input (pin 6). The non-inverting (+) input (pin 5) is connected to the wiper of P2 (10K) which is part of a voltage divider. [R28, (68k) - P2 (10k) - R29 (15k)] P2 is the injection STARTING POINT adjustment. Again, because of the rising ramp, the output voltage from IC9B (pin 7) is HIGH until it reaches the set point. Then it snaps LOW and this falling pulse triggers monostable IC8B (pin 11). Its output pulse length is determined by P3 (100k), R32 (15k) and C15 (68n). With these component values it is adjustable from about 1ms - 8.2ms. P3 is thus the SPEED (RPM) control of the engine. -:- So, we need to fire the injection solenoid somewhere during the inlet stroke for a period of about 1-8 ms. At 3750 RPM, the rpm of my Honda engine unloaded, 8 ms comes down to about 180 degrees of rotation of the crank shaft. Honda manual: http://www.quartermidgets.org/documents/Tech/Honda/2013_160_HONDA_TECH_MANUAL_%28UT2_Only%29.pdf The transistorized magneto ignition is fixed at 20 degrees BTDC and may not be altered in any way. Firing must not exceed 0.104 " or 20 degrees BTDC. */ #include "TimerHelpers.h" // Debug functions #define DEBUG #define PRINT_TICK_OUT_OF_RANGE_ERROR() \ Serial_Print ("Error! Tick nr "); \ Serial.print (nrInterrupts); \ Serial_Print (" ("); \ Serial.print (tick); \ Serial_Print (") Out of range. - Bye Bye."); \ Serial.println(); #ifdef DEBUG int freeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); } #endif /* // // Nice little trick to avoid const strings to be stored in SRAM. // See: http://linux.dd.com.au/wiki/Arduino_Static_Strings // #define Serial_Print(x) Serial_Print_P(PSTR(x)) void Serial_Print_P(PGM_P str) { for (uint8_t c; (c = pgm_read_byte(str)); str++) Serial.write(c); } #define Serial_Println(x) Serial_Print(x); Serial.println(); */ #define Serial_Print(x) Serial.print(F(x)) #define Serial_Println(x) Serial.println(F(x)) // Main variables and functions volatile unsigned long overflowCount; volatile unsigned int timer1CounterValue; // copy of TCNT1, see datasheet, page 117 (accessing 16-bit registers) volatile unsigned long exhaustHighTick = 0; volatile unsigned long exhaustLowTick = 0; volatile unsigned long curTick = 0; // prescale for timer 1 // 1, 8, 64, 256 or 1024 const byte t1_prescale = Timer1::PRESCALE_64; const unsigned long int tickFreq = F_CPU / 64; volatile float rpm_pulse = 0; // calculated from single exhaust valve pulse volatile float rpm_full = 0; // calculated from 2 full rotations volatile float exhaustOpenAngle = 167.0; // Opening angle of the exhaust valve volatile float exhaustOpenAngleCalc = 0; volatile bool exhaustAlreadyHighAtInit = false; // Use these macros for initialisation and array dimensioning only // Don't make this one too large. The amount of SRAM in these things is, // well, rather limited: // http://playground.arduino.cc/Main/CorruptArrayVariablesAndMemory // http://playground.arduino.cc/Learning/Memory #define MAX_NR_EDGES 16 //#define EXHAUST_PIN 10 #define INJECTION_PIN 11 #define IGNITION_PIN 12 #define EXHAUST_SENSOR_PIN 2 #define LED_PIN 13 #define DEBUG_PIN 9 const int ignitionPin = IGNITION_PIN; const int injectionPin = INJECTION_PIN; const int exhaustSensorPin = EXHAUST_SENSOR_PIN; const int ledPin = LED_PIN; class EdgeDef { public: unsigned int tick; byte pin, value; }; class InterruptDef { public: unsigned long tick; unsigned int overflowCount; EdgeDef edge; }; //Interrupt Table and book-keeping variables volatile byte currentInterrupt = -1; // no interrupt set volatile byte nrInterrupts = -1; volatile bool interruptTableInited = false; // table not initialised volatile InterruptDef interruptTable[MAX_NR_EDGES]; #define mOverFlowInt 1 #define mCompareInt 2 #define mExtInt0 3 // Logging / printing of interrupts #define mLogBufSize 12 #define mMaxLogEntries 150 // control variables volatile unsigned int logidx = 0; volatile unsigned int nLogEntries = 0; volatile unsigned int printidx = 0; volatile unsigned int nLogsPrinted = 0; // log variables volatile unsigned int intType[mLogBufSize]; volatile unsigned int ovlcount[mLogBufSize]; volatile unsigned int log_timer1CounterValue[mLogBufSize]; volatile unsigned int currentint[mLogBufSize]; volatile unsigned long currentTick[mLogBufSize]; volatile unsigned long lexhaustHighTick[mLogBufSize]; volatile unsigned long lexhaustLowTick[mLogBufSize]; volatile float pulserpm[mLogBufSize]; volatile float fullrpm[mLogBufSize]; volatile float log_exhaustOpenAngleCalc[mLogBufSize]; void logInterrupt( byte type ) { if ( logidx == mLogBufSize ) logidx = 0; if ( nLogEntries < mMaxLogEntries ) { ovlcount[logidx] = overflowCount; intType[logidx] = type; log_timer1CounterValue[logidx] = timer1CounterValue; currentint[logidx] = currentInterrupt; currentTick[logidx] = curTick; lexhaustHighTick[logidx] = exhaustHighTick; lexhaustLowTick[logidx] = exhaustLowTick; pulserpm[logidx] = rpm_pulse; fullrpm[logidx] = rpm_full; log_exhaustOpenAngleCalc[logidx] = exhaustOpenAngleCalc; logidx++; nLogEntries++; } } void printLog() { static bool headerPrinted = false; static bool footerPrinted = false; static unsigned int maxbufsz = 0; if ( !headerPrinted ) { Serial_Println("Log of: index - overflowctr - interrupt type - timer1CounterValue - current interrupt - current tick"); Serial_Println("Ext int: hightick - lowtick - rpm_pulse - rpm_full - exhaust angle"); headerPrinted = true; } if ( nLogsPrinted < nLogEntries && nLogsPrinted < mMaxLogEntries) { if ( maxbufsz < (nLogEntries - nLogsPrinted) ) maxbufsz = nLogEntries - nLogsPrinted; if ( printidx == mLogBufSize ) printidx = 0; Serial.print(nLogsPrinted + 1); Serial_Print(" - "); Serial.print(ovlcount[printidx]); Serial_Print(" - "); if ( intType[printidx] == mCompareInt ) Serial_Print("CMP"); else if ( intType[printidx] == mOverFlowInt ) Serial_Print("OVL"); else if ( intType[printidx] == mExtInt0 ) Serial_Print("EI0"); else Serial_Print("UNK"); Serial_Print(" - "); Serial.print( log_timer1CounterValue[printidx] ); Serial_Print(" - "); Serial.print( currentint[printidx] ); Serial_Print(" - "); Serial.println( currentTick[printidx] ); if ( intType[printidx] == mExtInt0 ) { Serial.print( lexhaustHighTick[printidx] ); Serial_Print(" - "); Serial.print( lexhaustLowTick[printidx] ); Serial_Print(" - "); Serial.print( pulserpm[printidx] ); Serial_Print(" - "); Serial.print( fullrpm[printidx] ); Serial_Print(" - "); Serial.println( log_exhaustOpenAngleCalc[printidx] ); } printidx++; nLogsPrinted++; } if ( !footerPrinted && nLogsPrinted >= mMaxLogEntries ) { Serial_Print("Max nr buffer entries needed: "); Serial.print(maxbufsz); Serial_Println(""); footerPrinted = true; } } // Don't use these in the code, use the (const) int versions. #undef MAX_NR_EDGES //#undef EXHAUST_SENSOR_PIN #undef INJECTION_PIN #undef IGNITION_PIN #undef LED_PIN // "Inline" functions // Look out for inline comments on lines with //; this works only at end of macro! inline void setCompareAInt( unsigned int tick ) { TIFR1 = bit (OCF1A); // clear flags so we don't get a bogus interrupt // Pending interrupts are cleared by writing a 1.... OCR1A = tick; TIMSK1 |= bit (OCIE1A); } inline void disableCompareAInt() { TIMSK1 &= ~bit (OCIE1A); // disable interrupt on Timer 1 on compare A Match } inline void resetTimer1() { TCNT1 = 0; // Counter to zero overflowCount = 0; // Therefore no overflows yet // curTick = 0; currentInterrupt = 0; disableCompareAInt(); } #define mResolution 10 inline void setupInterrupt( int idx, unsigned long atTick, byte pin, byte value, byte adjust ) { // adjust 0: no change allowed; < 0 : can be fired a bit earlier ; > ) : can be fired a bit later // Make sure we do not set two interrupts at the same tick. Only the first interrupt // can be non-adjustable. All others need to be moveable by 40 usec in one direction, unless // you are 100% sure the non-adjustable interrupts can never accidentally occur at the same // time. if ( adjust ) { bool changed = true; while ( changed ) { changed = false; for ( int cnt=0; cnt 0) ? mResolution : -mResolution; changed = true; } } } } } interruptTable[idx].tick = atTick; interruptTable[idx].overflowCount = atTick >> 16; interruptTable[idx].edge.tick = atTick & 0xFFFFL; interruptTable[idx].edge.pin = pin; interruptTable[idx].edge.value = value; } #undef mResolution #define SWAP(x) \ temp.x = interruptTable[j].x; \ interruptTable[j].x = interruptTable[j+1].x; \ interruptTable[j+1].x = temp.x; void sortInterruptTable() { // http://www.cquestions.com/2009/09/bubble-sort-using-c-program.html InterruptDef temp; //Bubble sorting algorithm for ( int i=nrInterrupts-2; i>=0; i--) { for ( int j=0; j<=i; j++) { if (interruptTable[j].tick > interruptTable[j+1].tick ) { SWAP(tick); SWAP(overflowCount); SWAP(edge.tick); SWAP(edge.pin); SWAP(edge.value); } } } } //#define DEBUG_INT_TAB #ifdef DEBUG_INT_TAB volatile bool doInitInterruptTab = false; #endif #define degreesToTicks(x) exhaustHighTick + (ticksPerDegreeOfRotation * (x) + 0.5) bool initInterruptTable( float rpm ) { #ifdef DEBUG_INT_TAB unsigned long startTick = (overflowCount << 16) + TCNT1; #endif if ( !rpm || !exhaustHighTick ) return false; interruptTableInited = false; digitalWrite(DEBUG_PIN,HIGH); nrInterrupts = 4; currentInterrupt = 0; float signalfreq = rpm / (2*60.0); // Full period is two rotations... float msecsPerDegreeOfRot = 1000.0 / ( signalfreq * 720.0); unsigned long totalNrTicks = (float) tickFreq / signalfreq; float ticksPerDegreeOfRotation = (float) totalNrTicks / 720.0; const float ticksPerMilliSec = tickFreq / 1000.0; const float tdc = 180 + exhaustOpenAngle + (180.0 - exhaustOpenAngle)/2; // assume symmetric // parameters for ignition and injection signals float ignition = -20.0; // adjust const float ignition_ms = 5.0; float injection = -10.0; // close injector about 10 degrees BTDC const float injection_ms = 6.0; // speed adust, can be 1 - 8 msec. // actual timing based on parameters unsigned long ignitionLowTick = degreesToTicks( tdc + ignition ); unsigned long ignitionHighTick = ignitionLowTick - ignition_ms * ticksPerMilliSec; unsigned long injectionLowTick = degreesToTicks( tdc + injection ); unsigned long injectionHighTick = injectionLowTick - injection_ms * ticksPerMilliSec; // reset ticks in tab so we can check which interrupts have been set. for ( int idx=0; idx= nrInterrupts ) { disableCompareAInt(); /* no more compare A interrupts for now */ digitalWrite(DEBUG_PIN,LOW); return; } if ( interruptTable[currentInterrupt].overflowCount == overflowCnt ) { setCompareAInt( interruptTable[currentInterrupt].edge.tick ); } else { disableCompareAInt(); /* no more compare A interrupts for now */ } } void prepareForInterrupts() { noInterrupts (); // protected code // External interrups 0 and 1 // enable INT0 interrupt for exhaust valve Hall sensor EICRA &= ~(bit(ISC00) | bit (ISC01)); // clear existing flags EICRA |= bit (ISC00); // set wanted flags (both rising and falling level interrupt) EIMSK |= bit (INT0); // enable it // enable INT1 interrupt for ingnition Hall sensor (optional) //EICRA &= ~(bit(ISC10) | bit (ISC11)); // clear existing flags //EICRA |= bit (ISC10); // set wanted flags (both rising and falling level interrupt) //EIMSK |= bit (INT1); // enable it // reset Timer 1 resetTimer1(); // Timer 1 - counts clock pulses //TIMSK1 = bit (TOIE1) | bit (ICIE1); // interrupt on Timer 1 overflow and input capture //TIMSK1 = bit (TOIE1) | bit (OCIE1A); // interrupt on Timer 1 overflow and compare A TIMSK1 = bit (TOIE1); // interrupt on Timer 1 overflow // Mode 0: Normal, top = 0xFFFF // Mode 4: CTC, top = OCR1A Timer1::setMode( 0, t1_prescale, Timer1::NO_PORT ); OCR1A = 0xFFFF; // Initialize count to maxint. // for CTC mode, Clear Timer on Count, we need to also set bit WGM21 . disableCompareAInt(); TIFR1 = bit (OCF1A) | bit (TOV1); // clear flags so we don't get a bogus interrupt // Pending interrupts are cleared by writing a 1.... // EIFR = 0; // clear pending interrupts EIFR = bit (INTF0); // clear flag for interrupt 0 EIFR = bit (INTF1); // clear flag for interrupt 1 if ( PIND & bit(EXHAUST_SENSOR_PIN) ) // exhaust signal high during startup... exhaustAlreadyHighAtInit = true; interrupts (); } // end of prepareForInterrupts // timer overflows (every 65536 counts) ISR (TIMER1_OVF_vect) { // grab counter value before it changes any more timer1CounterValue = TCNT1; // see datasheet, page 117 (accessing 16-bit registers) overflowCount++; curTick = (overflowCount << 16) + timer1CounterValue; setNextTimerInterrupt(overflowCount); logInterrupt( mOverFlowInt ); } // end of TIMER1_OVF_vect ISR (TIMER1_COMPA_vect) { // grab counter value before it changes any more timer1CounterValue = TCNT1; // see datasheet, page 117 (accessing 16-bit registers) // if just missed an overflow unsigned long overflowCopy = overflowCount; if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 0x7FFF) overflowCopy++; curTick = (overflowCopy << 16) + timer1CounterValue; byte pin=interruptTable[currentInterrupt].edge.pin; if ( pin >= 0 ) { digitalWrite( pin, interruptTable[currentInterrupt].edge.value ); } logInterrupt( mCompareInt ); currentInterrupt++; setNextTimerInterrupt(overflowCopy); } // end of TIMER1_COMPA_vect /* rpm = 60 * f f = 1 / T */ #ifdef DEBUG_INT_TAB volatile bool calculateAtExhaustRisingEdge = false; #endif // external interrupt connected to exhaust valve hall sensor ISR (INT0_vect) { #ifndef DEBUG_INT_TAB static bool calculateAtExhaustRisingEdge = false; #endif // grab counter value before it changes any more timer1CounterValue = TCNT1; // see datasheet, page 117 (accessing 16-bit registers) // if just missed an overflow unsigned long overflowCopy = overflowCount; if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 0x7FFF) overflowCopy++; curTick = (overflowCopy << 16) + timer1CounterValue; // For fast access, use direct port access instead of: // // byte value = digitalRead(exhaustSensorPin); // // EXHAUST_SENSOR_PIN should be 2, since it is connected to external int 0. // So, we need port D for direct access. byte value = PIND & bit(EXHAUST_SENSOR_PIN); if ( value ) // high { // All timing should be calculated using exhaustHighTick as reference if ( !exhaustAlreadyHighAtInit ) { // if exhaustHighTick, which still contains the previous value, equals 0, // then we did not yet finish full 4 strokes, so we can't // compute the rpm based on full rotation yet.... unsigned long prevexhaustHighTick = exhaustHighTick; // store previous tick exhaustHighTick = curTick; if ( exhaustHighTick && prevexhaustHighTick ) { float elapsed = (exhaustHighTick - prevexhaustHighTick) / (float) tickFreq; rpm_full = 120.0 / elapsed; // 60/1000 = 0.06 ; 0.06/2 = 0.03, 1 signal per two rotations } else { rpm_full = 0; } // reset overflow count at rising edge, when there is no risk of missing an overflow.. if ( (overflowCopy > 10) && (0x2000 < timer1CounterValue < 0x6000) ) { overflowCount = 0; overflowCopy = 0; curTick = timer1CounterValue; exhaustHighTick = timer1CounterValue; exhaustLowTick = 0; } if ( (rpm_full > 90) && (rpm_pulse > 90) ) calculateAtExhaustRisingEdge = true; if ( calculateAtExhaustRisingEdge ) { #ifdef DEBUG_INT_TAB doInitInterruptTab = true; #else if ( initInterruptTable(rpm_full) ) setNextTimerInterrupt(overflowCopy); #endif } } } else { // if exhaustLowTick, which still contains the previous value, equals 0, // then we did not yet finish full 4 strokes, so we can't // compute the rpm based on full rotation yet.... unsigned long prevexhaustLowTick = exhaustLowTick; // store previous tick exhaustLowTick = curTick; if ( exhaustHighTick ) { float exhaustOpenTime = (exhaustLowTick - exhaustHighTick) / (float) tickFreq; rpm_pulse = (exhaustOpenAngle / 360.0) * 60.0 / exhaustOpenTime; if ( exhaustLowTick && prevexhaustLowTick ) exhaustOpenAngleCalc = 360.0 * (exhaustLowTick - exhaustHighTick) / ((exhaustLowTick - prevexhaustLowTick)/2.0); else exhaustOpenAngleCalc = 0; if ( rpm_pulse < 60 ) calculateAtExhaustRisingEdge = false; if ( !calculateAtExhaustRisingEdge ) { #ifdef DEBUG_INT_TAB doInitInterruptTab = true; #else if ( initInterruptTable(rpm_pulse) ) setNextTimerInterrupt(overflowCopy); #endif } } else { // should only happen when exhaust already high during init of interrupts, etc. rpm_pulse = 0; } exhaustAlreadyHighAtInit = false; } digitalWrite(ledPin , value); logInterrupt( mExtInt0 ); } // end of TIMER1_INT0_vect void setup () { Serial.begin(115200); //Serial.begin(9600); Serial_Print("4 Stroke Engine Control System"); Serial.println(); #ifdef DEBUG Serial_Print ("Free SRAM: "); Serial.println(freeRam()); #endif pinMode(ignitionPin , OUTPUT); pinMode(injectionPin, OUTPUT); //set the LED pin as output pinMode(ledPin,OUTPUT); pinMode(DEBUG_PIN,OUTPUT); //set the interrupt sense pin INT0 as input pinMode(exhaustSensorPin, INPUT); //set the interrupt sense pin INT1 as input //pinMode(_INT1pin, INPUT); Serial_Println("processing initialization..."); // set up for interrupts prepareForInterrupts (); digitalWrite(ledPin,LOW); digitalWrite(DEBUG_PIN,LOW); if ( exhaustAlreadyHighAtInit ) Serial_Println("Exhaust high at startup."); else Serial_Println("Exhaust low at startup."); Serial_Println("Finished initialization"); } // end of setup void loop () { #ifdef DEBUG_INT_TAB static bool timerInterruptsConfigured = false; if ( doInitInterruptTab ) { timerInterruptsConfigured = initInterruptTable( calculateAtExhaustRisingEdge ? rpm_full : rpm_pulse ); if ( timerInterruptsConfigured ) { noInterrupts(); // protected code doInitInterruptTab = false; if ( timerInterruptsConfigured ) setNextTimerInterrupt(overflowCount); else disableCompareAInt(); interrupts(); } } //if ( timerInterruptsConfigured ) #endif printLog(); } // end of loop