// 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 TDC, when the inlet opens. degrees action ------- ------ 0 inlet valve open (TDC) 180 inlet valve close 180 - 360 compression (TDC) 370 ignition (HHO, 10 degrees after TDC) 370 - 540 work 540 exhaust valve openM 720 exhaust valve close 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-10 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 INLET_OUT_PIN 11 #define IGNITION_PIN 12 #define EXHAUST_SENSOR_PIN 2 #define LED_PIN 13 #define DEBUG_PIN 9 const int inletOutPin = INLET_OUT_PIN; const int exhaustPin = EXHAUST_PIN; const int ignitionPin = IGNITION_PIN; const int exhaustSensorPin = EXHAUST_SENSOR_PIN; const int ledPin = LED_PIN; typedef struct { unsigned int tick; byte pin, value; } EdgeDef; typedef struct { unsigned int overflowCount; EdgeDef edge; } InterruptDef; //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; } } // May become a settings structure, which may be read/stored in EEPROM. // Then we need a config ID. //#define CONFIG_VERSION "EngCtrlV0.01" struct SignalDef { int nrEdges; // Nr of edges in the signal(s) int nrTicks; // Nr of ticks for one full period of the signal(s) EdgeDef edges[MAX_NR_EDGES]; // char version[12]; // This is for mere detection if they are our signal } signal = { 6, // Nr of edges in signal 720, // Full period of signal: 720 degrees of rotation { // pairs of { tick, pin, value } // tick must be 1 ... nrTicks. { 181, EXHAUST_PIN , LOW }, // TDC, stroke 1, inlet open { 182, INLET_OUT_PIN, HIGH }, // TDC, stroke 1, inlet open { 270, IGNITION_PIN , HIGH }, // 20 defrees before TDC, stroke 3. { 360, INLET_OUT_PIN, LOW }, // 180 degrees after TDC, stroke 1, inlet close { 370, IGNITION_PIN , LOW }, // 10 degrees before TDC { 719, EXHAUST_PIN , HIGH }, // 10 degrees before TDC } // }, // CONFIG_VERSION }; // Don't use these in the code, use the (const) int versions. #undef MAX_NR_EDGES //#undef EXHAUST_SENSOR_PIN #undef INLET_OUT_PIN #undef IGNITION_PIN #undef EXHAUST_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 DEBUG_INT_TAB #ifdef DEBUG_INT_TAB volatile bool doInitInterruptTab = false; #endif bool initInterruptTable() { #ifdef DEBUG_INT_TAB unsigned long startTick = (overflowCount << 16) + TCNT1; #endif if ( !rpm_pulse || !exhaustHighTick ) return false; interruptTableInited = false; digitalWrite(DEBUG_PIN,HIGH); nrInterrupts = 0; currentInterrupt = 0; float signalfreq = rpm_pulse / (2*60.0); // Full period is two rotations... float msecsPerSignalTick = 1000.0 / ( signalfreq * signal.nrTicks); unsigned long totalNrTicks = (float) tickFreq / signalfreq; //should be 24000000 at 10 RPM float ticksPerSignalTick = (float) totalNrTicks / signal.nrTicks; for ( nrInterrupts=0; nrInterrupts < signal.nrEdges; nrInterrupts++ ) { unsigned int tick = signal.edges[nrInterrupts].tick; if ( !tick || tick > signal.nrTicks ) { PRINT_TICK_OUT_OF_RANGE_ERROR(); return false; } unsigned long int interruptAtTick = exhaustHighTick + (ticksPerSignalTick * tick + 0.5); interruptTable[nrInterrupts].overflowCount = interruptAtTick >> 16; interruptTable[nrInterrupts].edge.tick = interruptAtTick & 0xFFFFL; interruptTable[nrInterrupts].edge.pin = signal.edges[nrInterrupts].pin; interruptTable[nrInterrupts].edge.value = signal.edges[nrInterrupts].value; } interruptTableInited = true; #ifdef DEBUG_INT_TAB unsigned long stopTick = (overflowCount << 16) + TCNT1; static int nr_debug_printed = 0; #define mMaxNrDebugPrints 10 if ( nr_debug_printed < mMaxNrDebugPrints ) { Serial_Print ("Free SRAM: "); Serial.println (freeRam()); Serial_Print ("RPM: "); Serial.println (rpm_pulse); Serial_Print ("signal freq: "); Serial.println (signalfreq); Serial_Print ("msecsPerSignalTick: "); Serial.println (msecsPerSignalTick); Serial_Print ("tickFreq: "); Serial.println (tickFreq); Serial_Print ("totalNrTicks: "); Serial.println (totalNrTicks); Serial_Print ("ticksPerSignalTick: "); Serial.println (ticksPerSignalTick); Serial_Print ("Exhaust high tick: "); Serial.println (exhaustHighTick); Serial_Print ("Start tick: "); Serial.println (startTick); Serial_Print ("Stop tick: "); Serial.println (stopTick); float usecCalcTime = 1000000.0 * float(stopTick-startTick)/tickFreq; Serial_Print ("Calculation took "); Serial.print (usecCalcTime); Serial_Println (" usecs."); Serial_Print ("That's: "); Serial.print (usecCalcTime/44.4); Serial_Println (" degrees of rotation at 3750 RPM."); Serial_Print ("Or: "); Serial.print (usecCalcTime/463.0); Serial_Println (" degrees of rotation at 360 RPM."); Serial_Print ("Or: "); Serial.print (usecCalcTime/2777.778); Serial_Println (" degrees of rotation at 60 RPM."); Serial_Print ("Current tick: "); Serial.println (curTick); for ( int cnt=0; cnt < nrInterrupts; cnt++) { Serial_Print ("--------------------"); Serial.println (); Serial_Print ("Interrupt nr: "); Serial.println (cnt); Serial_Print ("at tick: "); Serial.println (( ((unsigned long)interruptTable[cnt].overflowCount) << 16) + interruptTable[cnt].edge.tick); Serial_Print ("Nr overflows: "); Serial.println (interruptTable[cnt].overflowCount); Serial_Print ("compareVal: "); Serial.println (interruptTable[cnt].edge.tick); } } nr_debug_printed++; #endif return true; } void setNextTimerInterrupt( unsigned long overflowCnt ) { if ( !interruptTableInited ) return; if ( currentInterrupt >= 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 */ // external interrupt connected to exhaust valve hall sensor ISR (INT0_vect) { static bool calculateAtExhaustRisingEdge = false; // 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() ) 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() ) 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(inletOutPin, OUTPUT); pinMode(exhaustPin , OUTPUT); pinMode(ignitionPin, 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(); if ( timerInterruptsConfigured ) { noInterrupts(); // protected code doInitInterruptTab = false; currentInterrupt = 0; if ( timerInterruptsConfigured ) setNextTimerInterrupt(overflowCount); else disableCompareAInt(); interrupts(); } } //if ( timerInterruptsConfigured ) #endif printLog(); } // end of loop