From 6289f0cda268c0badadb176d76ad91520065fe7e Mon Sep 17 00:00:00 2001 From: darren siepka Date: Thu, 13 Oct 2016 23:24:22 +0100 Subject: [PATCH 1/4] update with josh2 13102016 --- auxiliaries.h | 7 +- auxiliaries.ino | 11 +- cancomms.h | 4 +- cancomms.ino | 2 +- comms.ino | 128 ++++++++------- decoders.h | 2 +- decoders.ino | 9 +- globals.h | 14 +- idle.h | 3 +- idle.ino | 34 +++- reference/Speeduino base tune.msq | 133 +++++++-------- reference/speeduino.ini | 39 +++-- scheduler.h | 136 +++++++++++++-- scheduler.ino | 264 ++++++++++++++++++------------ speeduino.ino | 216 ++++++++++++++---------- timers.h | 4 +- timers.ino | 7 +- utils.ino | 4 + 18 files changed, 642 insertions(+), 375 deletions(-) diff --git a/auxiliaries.h b/auxiliaries.h index 38b9126..42cdf2a 100644 --- a/auxiliaries.h +++ b/auxiliaries.h @@ -1,4 +1,9 @@ +#ifndef AUX_H +#define AUX_H +void initialiseAuxPWM(); +void boostControl(); +void vvtControl(); volatile byte *boost_pin_port; volatile byte boost_pin_mask; @@ -16,4 +21,4 @@ unsigned int vvt_pwm_max_count; //Used for variable PWM frequency volatile unsigned int vvt_pwm_cur_value; long vvt_pwm_target_value; - +#endif diff --git a/auxiliaries.ino b/auxiliaries.ino index 4da2526..17e4673 100644 --- a/auxiliaries.ino +++ b/auxiliaries.ino @@ -21,6 +21,7 @@ void fanControl() else if (currentStatus.coolant <= (configPage4.fanSP - configPage4.fanHyster)) { digitalWrite(pinFan, fanLOW); } } +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) void initialiseAuxPWM() { TCCR1B = 0x00; //Disbale Timer1 while we set it up @@ -53,9 +54,7 @@ void boostControl() boostPID.Compute(); TIMSK1 |= (1 << OCIE1A); //Turn on the compare unit (ie turn on the interrupt) } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) else { TIMSK1 &= ~(1 << OCIE1A); } // Disable timer channel -#endif } void vvtControl() @@ -71,7 +70,6 @@ void vvtControl() } //The interrupt to control the Boost PWM -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) ISR(TIMER1_COMPA_vect) { if (boost_pwm_state) @@ -107,6 +105,11 @@ ISR(TIMER1_COMPB_vect) } } -#elif defined(PROCESSOR_TEENSY_3_1) || defined(PROCESSOR_TEENSY_3_2) +#elif defined (CORE_TEENSY) +//YET TO BE IMPLEMENTED ON TEENSY +void initialiseAuxPWM() { } +void boostControl() { } +void vvtControl() { } + #endif diff --git a/cancomms.h b/cancomms.h index e254a0f..bf87ed6 100644 --- a/cancomms.h +++ b/cancomms.h @@ -4,14 +4,14 @@ #define veMapPage 1 -uint8_t currentcanPage = 1;//Not the same as the speeduino config page numbers +uint8_t currentCanPage = 1;//Not the same as the speeduino config page numbers uint8_t nCanretry = 0; //no of retrys uint8_t cancmdfail = 0; //command fail yes/no uint8_t canlisten = 0; uint8_t Lbuffer[8]; //8 byte buffer to store incomng can data -void Cancommand();//This is the heart of the Command Line Interpeter. All that needed to be done was to make it human readable. +void canCommand();//This is the heart of the Command Line Interpeter. All that needed to be done was to make it human readable. void sendCancommand(uint8_t cmdtype , uint16_t canadddress, uint8_t candata1, uint8_t candata2); void testCanComm(); diff --git a/cancomms.ino b/cancomms.ino index c3f1cd9..8a840a4 100644 --- a/cancomms.ino +++ b/cancomms.ino @@ -15,7 +15,7 @@ sendcancommand is called when a comman d is to be sent via serial3 to the Can in //#include "globals.h" //#include "storage.h" -void Cancommand() +void canCommand() { switch (Serial3.read()) { diff --git a/comms.ino b/comms.ino index 6b0fe54..26a6c8d 100644 --- a/comms.ino +++ b/comms.ino @@ -18,7 +18,7 @@ void command() switch (Serial.read()) { case 'A': // send x bytes of realtime values - sendValues(packetSize,0); //send values to serial0 + sendValues(packetSize, 0); //send values to serial0 break; case 'B': // Burn current values to eeprom @@ -62,12 +62,12 @@ void command() break; case 'S': // send code version - Serial.print("Speeduino 2016.09"); + Serial.print("Speeduino 2016.10-dev"); currentStatus.secl = 0; //This is required in TS3 due to its stricter timings break; case 'Q': // send code version - Serial.print("speeduino 201609-dev"); + Serial.print("speeduino 201610-dev"); break; case 'V': // send VE table and constants in binary @@ -75,7 +75,7 @@ void command() break; case 'W': // receive new VE obr constant at 'W'++ - int offset; + int valueOffset; //cannot use offset as a variable name, it is a reserved word for several teensy libraries while (Serial.available() == 0) { } if (isMap) @@ -84,15 +84,15 @@ void command() offset1 = Serial.read(); while (Serial.available() == 0) { } offset2 = Serial.read(); - offset = word(offset2, offset1); + valueOffset = word(offset2, offset1); } else { - offset = Serial.read(); + valueOffset = Serial.read(); } while (Serial.available() == 0) { } - receiveValue(offset, Serial.read()); + receiveValue(valueOffset, Serial.read()); break; case 't': // receive new Calibration info. Command structure: "t", . This is an MS2/Extra command, NOT part of MS1 spec @@ -198,20 +198,21 @@ void command() /* This function returns the current values of a fixed group of variables */ -void sendValues(int packetlength, byte portnum) +void sendValues(int packetlength, byte portNum) { byte response[packetlength]; - if (portnum == 3){ //if port number is 3 + if (portNum == 3) + { + //CAN serial Serial3.write("A"); //confirm cmd type Serial3.write(packetlength); //confirm no of byte to be sent - } - + } else - { - if(requestCount == 0) { currentStatus.secl = 0; } - requestCount++; - } + { + if(requestCount == 0) { currentStatus.secl = 0; } + requestCount++; + } currentStatus.spark ^= (-currentStatus.hasSync ^ currentStatus.spark) & (1 << BIT_SPARK_SYNC); //Set the sync bit of the Spark variable to match the hasSync variable @@ -260,14 +261,13 @@ void sendValues(int packetlength, byte portnum) response[34] = getNextError(); //cli(); - if (portnum == 0){Serial.write(response, (size_t)packetlength);} - else if (portnum == 3){Serial3.write(response, (size_t)packetlength);} - //Serial.flush(); + if (portNum == 0) { Serial.write(response, (size_t)packetlength); } + else if (portNum == 3) { Serial3.write(response, (size_t)packetlength); } //sei(); return; } -void receiveValue(int offset, byte newValue) +void receiveValue(int valueOffset, byte newValue) { void* pnt_configPage;//This only stores the address of the value that it's pointing to and not the max size @@ -275,24 +275,24 @@ void receiveValue(int offset, byte newValue) switch (currentPage) { case veMapPage: - if (offset < 256) //New value is part of the fuel map + if (valueOffset < 256) //New value is part of the fuel map { - fuelTable.values[15 - offset / 16][offset % 16] = newValue; + fuelTable.values[15 - valueOffset / 16][valueOffset % 16] = newValue; return; } else { //Check whether this is on the X (RPM) or Y (MAP/TPS) axis - if (offset < 272) + if (valueOffset < 272) { //X Axis - fuelTable.axisX[(offset - 256)] = ((int)(newValue) * 100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct + fuelTable.axisX[(valueOffset - 256)] = ((int)(newValue) * 100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct } else { //Y Axis - offset = 15 - (offset - 272); //Need to do a translation to flip the order (Due to us using (0,0) in the top left rather than bottom right - fuelTable.axisY[offset] = (int)(newValue); + valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order (Due to us using (0,0) in the top left rather than bottom right + fuelTable.axisY[valueOffset] = (int)(newValue); } return; } @@ -301,31 +301,31 @@ void receiveValue(int offset, byte newValue) case veSetPage: pnt_configPage = &configPage1; //Setup a pointer to the relevant config page //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size - if ( offset < page_size) + if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)offset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages } break; case ignMapPage: //Ignition settings page (Page 2) - if (offset < 256) //New value is part of the ignition map + if (valueOffset < 256) //New value is part of the ignition map { - ignitionTable.values[15 - offset / 16][offset % 16] = newValue; + ignitionTable.values[15 - valueOffset / 16][valueOffset % 16] = newValue; return; } else { //Check whether this is on the X (RPM) or Y (MAP/TPS) axis - if (offset < 272) + if (valueOffset < 272) { //X Axis - ignitionTable.axisX[(offset - 256)] = (int)(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct + ignitionTable.axisX[(valueOffset - 256)] = (int)(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct } else { //Y Axis - offset = 15 - (offset - 272); //Need to do a translation to flip the order - ignitionTable.axisY[offset] = (int)(newValue); + valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order + ignitionTable.axisY[valueOffset] = (int)(newValue); } return; } @@ -333,31 +333,31 @@ void receiveValue(int offset, byte newValue) case ignSetPage: pnt_configPage = &configPage2; //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size - if ( offset < page_size) + if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)offset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages } break; case afrMapPage: //Air/Fuel ratio target settings page - if (offset < 256) //New value is part of the afr map + if (valueOffset < 256) //New value is part of the afr map { - afrTable.values[15 - offset / 16][offset % 16] = newValue; + afrTable.values[15 - valueOffset / 16][valueOffset % 16] = newValue; return; } else { //Check whether this is on the X (RPM) or Y (MAP/TPS) axis - if (offset < 272) + if (valueOffset < 272) { //X Axis - afrTable.axisX[(offset - 256)] = int(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiply it back by 100 to make it correct + afrTable.axisX[(valueOffset - 256)] = int(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiply it back by 100 to make it correct } else { //Y Axis - offset = 15 - (offset - 272); //Need to do a translation to flip the order - afrTable.axisY[offset] = int(newValue); + valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order + afrTable.axisY[valueOffset] = int(newValue); } return; @@ -366,52 +366,52 @@ void receiveValue(int offset, byte newValue) case afrSetPage: pnt_configPage = &configPage3; //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size - if ( offset < page_size) + if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)offset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages } break; case iacPage: //Idle Air Control settings page (Page 4) pnt_configPage = &configPage4; //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size - if ( offset < page_size) + if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)offset) = newValue; + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; } break; case boostvvtPage: //Boost and VVT maps (8x8) - if (offset < 64) //New value is part of the boost map + if (valueOffset < 64) //New value is part of the boost map { - boostTable.values[7 - offset / 8][offset % 8] = newValue; + boostTable.values[7 - valueOffset / 8][valueOffset % 8] = newValue; return; } - else if (offset < 72) //New value is on the X (RPM) axis of the boost table + else if (valueOffset < 72) //New value is on the X (RPM) axis of the boost table { - boostTable.axisX[(offset - 64)] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct + boostTable.axisX[(valueOffset - 64)] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct return; } - else if (offset < 80) //New value is on the Y (TPS) axis of the boost table + else if (valueOffset < 80) //New value is on the Y (TPS) axis of the boost table { - boostTable.axisY[(7 - (offset - 72))] = int(newValue); + boostTable.axisY[(7 - (valueOffset - 72))] = int(newValue); return; } - else if (offset < 144) //New value is part of the vvt map + else if (valueOffset < 144) //New value is part of the vvt map { - offset = offset - 80; - vvtTable.values[7 - offset / 8][offset % 8] = newValue; + valueOffset = valueOffset - 80; + vvtTable.values[7 - valueOffset / 8][valueOffset % 8] = newValue; return; } - else if (offset < 152) //New value is on the X (RPM) axis of the vvt table + else if (valueOffset < 152) //New value is on the X (RPM) axis of the vvt table { - offset = offset - 144; - vvtTable.axisX[offset] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct + valueOffset = valueOffset - 144; + vvtTable.axisX[valueOffset] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct return; } else //New value is on the Y (Load) axis of the vvt table { - offset = offset - 152; - vvtTable.axisY[(7 - offset)] = int(newValue); + valueOffset = valueOffset - 152; + vvtTable.axisY[(7 - valueOffset)] = int(newValue); return; } default: @@ -777,7 +777,7 @@ This function is used to store calibration data sent by Tuner Studio. void receiveCalibration(byte tableID) { byte* pnt_TargetTable; //Pointer that will be used to point to the required target table - int OFFSET, DIVISION_FACTOR, BYTES_PER_VALUE; + int OFFSET, DIVISION_FACTOR, BYTES_PER_VALUE, EEPROM_START; switch (tableID) { @@ -787,6 +787,7 @@ void receiveCalibration(byte tableID) OFFSET = CALIBRATION_TEMPERATURE_OFFSET; // DIVISION_FACTOR = 10; BYTES_PER_VALUE = 2; + EEPROM_START = EEPROM_CALIBRATION_CLT; break; case 1: //Inlet air temp table @@ -794,6 +795,7 @@ void receiveCalibration(byte tableID) OFFSET = CALIBRATION_TEMPERATURE_OFFSET; DIVISION_FACTOR = 10; BYTES_PER_VALUE = 2; + EEPROM_START = EEPROM_CALIBRATION_IAT; break; case 2: //O2 table @@ -801,6 +803,7 @@ void receiveCalibration(byte tableID) OFFSET = 0; DIVISION_FACTOR = 1; BYTES_PER_VALUE = 1; + EEPROM_START = EEPROM_CALIBRATION_O2; break; default: @@ -848,7 +851,10 @@ void receiveCalibration(byte tableID) } pnt_TargetTable[(x / 2)] = (byte)tempValue; - int y = EEPROM_CALIBRATION_O2 + counter; + + //From TS3.x onwards, the EEPROM must be written here as TS restarts immediately after the process completes which is before the EEPROM write completes + int y = EEPROM_START + (x / 2); + EEPROM.update(y, (byte)tempValue); every2nd = false; analogWrite(13, (counter % 50) ); diff --git a/decoders.h b/decoders.h index 0bec10f..c2185a5 100644 --- a/decoders.h +++ b/decoders.h @@ -15,7 +15,7 @@ volatile unsigned long toothLastSecToothTime = 0; //The time (micros()) that the volatile unsigned long toothLastMinusOneToothTime = 0; //The time (micros()) that the tooth before the last tooth was registered volatile unsigned long toothOneTime = 0; //The time (micros()) that tooth 1 last triggered volatile unsigned long toothOneMinusOneTime = 0; //The 2nd to last time (micros()) that tooth 1 last triggered -volatile bool revolutionOne; // For sequential operation, this tracks whether the current revolution is 1 or 2 (not 1) +volatile bool revolutionOne = 0; // For sequential operation, this tracks whether the current revolution is 1 or 2 (not 1) volatile unsigned int toothHistory[TOOTH_LOG_BUFFER]; volatile unsigned int toothHistoryIndex = 0; diff --git a/decoders.ino b/decoders.ino index e09f7e3..2dd223a 100644 --- a/decoders.ino +++ b/decoders.ino @@ -107,6 +107,7 @@ void triggerPri_missingTooth() if ( curGap > targetGap || toothCurrentCount > triggerActualTeeth) { toothCurrentCount = 1; + revolutionOne = !revolutionOne; //Flip sequential revolution tracker toothOneMinusOneTime = toothOneTime; toothOneTime = curTime; currentStatus.hasSync = true; @@ -123,7 +124,10 @@ void triggerPri_missingTooth() toothLastToothTime = curTime; } -void triggerSec_missingTooth(){ return; } //This function currently is not used +void triggerSec_missingTooth() +{ + if(!currentStatus.hasSync) { revolutionOne = 0; } //Sequential revolution reset +} int getRPM_missingTooth() { @@ -147,6 +151,9 @@ int getCrankAngle_missingTooth(int timePerDegree) if(elapsedTime < SHRT_MAX ) { crankAngle += div((int)elapsedTime, timePerDegree).quot; } //This option is much faster, but only available for smaller values of elapsedTime else { crankAngle += ldiv(elapsedTime, timePerDegree).quot; } + //Sequential check (simply sets whether we're on the first or 2nd revoltuion of the cycle) + if (revolutionOne) { crankAngle += 360; } + if (crankAngle >= 720) { crankAngle -= 720; } else if (crankAngle > CRANK_ANGLE_MAX) { crankAngle -= CRANK_ANGLE_MAX; } if (crankAngle < 0) { crankAngle += 360; } diff --git a/globals.h b/globals.h index 4ee3105..5b88dc0 100644 --- a/globals.h +++ b/globals.h @@ -55,9 +55,15 @@ const byte packetSize = 35; #define TOOTH_LOG_SIZE 128 #define TOOTH_LOG_BUFFER 256 -#define INJ_SIMULTANEOUS 0 +#define INJ_PAIRED 0 #define INJ_SEMISEQUENTIAL 1 -#define INJ_SEQUENTIAL 2 +#define INJ_BANKED 2 +#define INJ_SEQUENTIAL 3 + +#define IGN_MODE_WASTED 0 +#define IGN_MODE_SINGLE 1 +#define IGN_MODE_WASTEDCOP 2 +#define IGN_MODE_SEQUENTIAL 3 #define SIZE_BYTE 8 #define SIZE_INT 16 @@ -141,7 +147,7 @@ struct statuses { unsigned int PW; //In uS volatile byte runSecs; //Counter of seconds since cranking commenced (overflows at 255 obviously) volatile byte secl; //Continous - volatile int loopsPerSecond; + volatile unsigned int loopsPerSecond; boolean launchingSoft; //True when in launch control soft limit mode boolean launchingHard; //True when in launch control hard limit mode int freeRAM; @@ -215,7 +221,7 @@ struct config1 { byte algorithm : 1; //"Speed Density", "Alpha-N" byte baroCorr : 1; byte injLayout : 2; - byte canenable : 1; //is can interface enabled + byte canEnable : 1; //is can interface enabled byte primePulse; byte dutyLim; diff --git a/idle.h b/idle.h index f2a2552..dbc7413 100644 --- a/idle.h +++ b/idle.h @@ -36,4 +36,5 @@ long idle_pwm_target_value; long idle_cl_target_rpm; void initialiseIdle(); - +static inline void disableIdle(); +static inline void enableIdle(); diff --git a/idle.ino b/idle.ino index 4d75065..3b8e431 100644 --- a/idle.ino +++ b/idle.ino @@ -53,7 +53,7 @@ void initialiseIdle() idle2_pin_port = portOutputRegister(digitalPinToPort(pinIdle2)); idle2_pin_mask = digitalPinToBitMask(pinIdle2); idle_pwm_max_count = 1000000L / (16 * configPage3.idleFreq * 2); //Converts the frequency in Hz to the number of ticks (at 16uS) it takes to complete 1 cycle. Note that the frequency is divided by 2 coming from TS to allow for up to 512hz - TIMSK4 |= (1 << OCIE4C); //Turn on the C compare unit (ie turn on the interrupt) + enableIdle(); break; case 3: @@ -141,8 +141,8 @@ void idleControl() { //Standard running currentStatus.idleDuty = table2D_getValue(&iacPWMTable, currentStatus.coolant + CALIBRATION_TEMPERATURE_OFFSET); //All temps are offset by 40 degrees - if( currentStatus.idleDuty == 0 ) { TIMSK4 &= ~(1 << OCIE4C); digitalWrite(pinIdle1, LOW); break; } - TIMSK4 |= (1 << OCIE4C); //Turn on the C compare unit (ie turn on the interrupt) + if( currentStatus.idleDuty == 0 ) { disableIdle(); break; } + enableIdle(); idle_pwm_target_value = percentage(currentStatus.idleDuty, idle_pwm_max_count); idleOn = true; } @@ -154,8 +154,8 @@ void idleControl() //idlePID.SetTunings(configPage3.idleKP, configPage3.idleKI, configPage3.idleKD); idlePID.Compute(); - if( idle_pwm_target_value == 0 ) { TIMSK4 &= ~(1 << OCIE4C); digitalWrite(pinIdle1, LOW); } - else{ TIMSK4 |= (1 << OCIE4C); } //Turn on the C compare unit (ie turn on the interrupt) + if( idle_pwm_target_value == 0 ) { disableIdle(); } + else{ enableIdle(); } //Turn on the C compare unit (ie turn on the interrupt) //idle_pwm_target_value = 104; break; @@ -240,6 +240,20 @@ void homeStepper() //The interrupt to turn off the idle pwm #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +//This function simply turns off the idle PWM and sets the pin low +static inline void disableIdle() +{ + TIMSK4 &= ~(1 << OCIE4C); //Turn off interrupt + digitalWrite(pinIdle1, LOW); +} + +//Any common functions associated with starting the Idle +//Typically this is enabling the PWM interrupt +static inline void enableIdle() +{ + TIMSK4 |= (1 << OCIE4C); //Turn on the C compare unit (ie turn on the interrupt) +} + ISR(TIMER4_COMPC_vect) { if (idle_pwm_state) @@ -279,6 +293,12 @@ ISR(TIMER4_COMPC_vect) } } -#elif defined(PROCESSOR_TEENSY_3_1) || defined(PROCESSOR_TEENSY_3_2) -void idle_off() { } +#elif defined (CORE_TEENSY) +//This function simply turns off the idle PWM and sets the pin low +static inline void disableIdle() +{ + digitalWrite(pinIdle1, LOW); +} + +static inline void enableIdle() { } #endif diff --git a/reference/Speeduino base tune.msq b/reference/Speeduino base tune.msq index b3cecc6..0c6310e 100644 --- a/reference/Speeduino base tune.msq +++ b/reference/Speeduino base tune.msq @@ -1,7 +1,7 @@ - - + + "0" @@ -79,7 +79,7 @@ 100.0 20.0 -"Speeduino v0.3" +"Speeduino v0.4" "Board Default" "One" 3.2 @@ -94,7 +94,7 @@ "RPM" "RPM" "RPM" -14.4 +12.8 2.0 "Alternating" "No" @@ -116,7 +116,6 @@ "Speed Density" "Off" "Bank" -"Enable" 4.0 85.0 4200.0 @@ -128,6 +127,9 @@ 260.0 3.0 14.7 +0.0 +0.0 +0.0 0.0 0.0 0.0 @@ -196,16 +198,14 @@ 0.0 0.0 5.0 -10.0 +4.0 "Leading" "Crank Speed" "Going Low" "Yes" "Missing Tooth" --1.9008 -65.0 3200.0 --5.814 +-21.0 38.0 2.0 "Dwell control" @@ -236,17 +236,17 @@ 17.0 32.0 - - -40.014 - -14.814 - 17.586 - 48.186 - 78.786 - 100.386 - 120.186 - 139.986 - 156.186 - 175.986 + + -40.0 + -26.0 + -8.0 + 9.0 + 26.0 + 38.0 + 49.0 + 60.0 + 69.0 + 80.0 8.0 @@ -257,13 +257,13 @@ 91.0 85.0 - - 136.386 - 179.586 - 199.386 - 219.186 - 240.786 - 256.986 + + 58.0 + 82.0 + 93.0 + 104.0 + 116.0 + 140.0 0.0 @@ -271,7 +271,7 @@ 2.0 4.0 6.0 - 8.0 + 10.0 1500.0 200.0 @@ -345,7 +345,7 @@ 100.0 20.0 0.0 -157.986 +70.0 16.0 1.0 15.0 @@ -372,16 +372,16 @@ 100.0 98.0 - - -40.014 - -4.014 - 31.986 - 67.986 - 94.986 - 121.986 - 139.986 - 193.986 - 247.986 + + -40.0 + -20.0 + 0.0 + 20.0 + 35.0 + 50.0 + 60.0 + 90.0 + 120.0 126.0 @@ -411,7 +411,8 @@ 100.0 0.0 0.0 -24300.0 +"Float" +"ONE" 4500.0 3000.0 6000.0 @@ -453,17 +454,17 @@ 16.0 9.0 - - -36.414 - -2.214 - 33.786 - 62.586 - 93.186 - 121.986 - 145.386 - 174.186 - 208.386 - 289.386 + + -38.0 + -19.0 + 1.0 + 17.0 + 34.0 + 50.0 + 63.0 + 79.0 + 98.0 + 143.0 123.0 @@ -477,17 +478,17 @@ 44.0 60.0 - - -18.414 - 42.786 - 111.186 - 168.786 + + -28.0 + 6.0 + 44.0 + 76.0 "None" "3" "1" "Normal" -67.986 +20.0 240.0 4.0 "No" @@ -497,14 +498,14 @@ "No" "No" "No" -166.986 -35.586 +75.0 +2.0 6.0 - - 139.986 - -4.014 - -40.014 - 316.386 + + 60.0 + -20.0 + -40.0 + 158.0 @@ -570,9 +571,9 @@ - + - + \ No newline at end of file diff --git a/reference/speeduino.ini b/reference/speeduino.ini index 5536222..e9a61b6 100644 --- a/reference/speeduino.ini +++ b/reference/speeduino.ini @@ -6,7 +6,7 @@ queryCommand = "Q" ;signature = 20 - signature = "speeduino 201609-dev" + signature = "speeduino 201610-dev" versionInfo = "S" ; Put this in the title bar. @@ -160,7 +160,6 @@ page = 2 reqFuel = scalar, U08, 24, "ms", 0.1, 0.0, 0.0, 25.5, 1 divider = scalar, U08, 25, "", 1.0, 0.0 - ;injTiming = bits, U08, 26, [0:1], "Simultaneous", "Semi-Sequential", "Sequential" alternate = bits, U08, 26, [0:0], "Simultaneous", "Alternating" multiplyMAP= bits, U08, 26, [1:1], "No", "Yes" includeAFR = bits, U08, 26, [2:2], "No", "Yes" @@ -186,8 +185,8 @@ page = 2 flexEnabled= bits, U08, 38, [1:1], "Off", "On" algorithm = bits, U08, 38, [2:2], "Speed Density", "Alpha-N" baroCorr = bits, U08, 38, [3:3], "Off", "On" - injLayout = bits, U08, 38, [4:5], "Bank", "Semi-Sequential", "INVALID", "INVALID" - canenable = bits, U08, 38, [6:6], "Disable", "Enable" + injLayout = bits, U08, 38, [4:5], "Paired", "Semi-Sequential", "INVALID", "Sequential" + canEnable = bits, U08, 38, [6:6], "Disable", "Enable" primePulse = scalar, U08, 39, "ms", 0.1, 0.0, 0.0, 25.5, 1 dutyLim = scalar, U08, 40, "%", 1.0, 0.0, 0.0, 100.0, 0 @@ -336,9 +335,9 @@ page = 6 egoKI = scalar, U08, 2, "%", 1.0, 0.0, 0.0, 200.0, 0 ; * ( 1 byte) egoKD = scalar, U08, 3, "%", 1.0, 0.0, 0.0, 200.0, 0 ; * ( 1 byte) #if CELSIUS - egoTemp = scalar, U08, 4, "°C", 1.0, -40, -40, 102.0, 0 + egoTemp = scalar, U08, 4, "C", 1.0, -40, -40, 102.0, 0 #else - egoTemp = scalar, U08, 4, "°F", 1.8, -22.23, -40, 215.0, 0 + egoTemp = scalar, U08, 4, "F", 1.8, -22.23, -40, 215.0, 0 #endif egoCount = scalar, U08, 5, "", 4.0, 0.0, 4.0, 255.0, 0 ; * ( 1 byte) egoDelta = scalar, U08, 6, "%", 1.0, 0.0, 0.0, 255.0, 0 ; * ( 1 byte) @@ -431,11 +430,11 @@ page = 7 unused7-55e = bits, U08, 56, [6:6], "No", "Yes" unused7-55f = bits, U08, 56, [7:7], "No", "Yes" #if CELSIUS - fanSP = scalar, U08, 57, "°C", 1.0, -40, -40, 215.0, 0 - fanHyster = scalar, U08, 58, "°C", 1.0, -40, -40, 215.0, 0 + fanSP = scalar, U08, 57, "C", 1.0, -40, -40, 215.0, 0 + fanHyster = scalar, U08, 58, "C", 1.0, -40, -40, 215.0, 0 #else - fanSP = scalar, U08, 57, "°F", 1.8, -22.23, -40, 215.0, 0 - fanHyster = scalar, U08, 58, "°F", 1.8, -22.23, -40, 215.0, 0 + fanSP = scalar, U08, 57, "F", 1.8, -22.23, -40, 215.0, 0 + fanHyster = scalar, U08, 58, "F", 1.8, -22.23, -40, 215.0, 0 #endif fanFreq = scalar, U08 , 59, "Hz", 2.0, 0.0, 10, 511, 0 #if CELSIUS @@ -590,8 +589,8 @@ menuDialog = main subMenu = vvtTbl, "VVT duty cycle", 8, { vvtEnabled } subMenu = std_separator subMenu = tacho, "Tacho Output" - subMenu = std_separator - subMenu = canio, "Canbus Interface" + subMenu = std_separator + subMenu = canIO, "Canbus Interface" @@ -619,11 +618,13 @@ menuDialog = main ; tool tips tooltips ;Ensure all settings are defined as some MS2/BG words shipped with TS are not applicable. nCylinders = "The number of cylinders in your engine." + alternate = "" engineType = "Most engines are Even Fire. Typical odd-fire engines are V-twin, some V4, Vmax, some V6, V10." twoStroke = "Four-Stroke (most engines), Two-stroke." nInjectors = "Number of primary injectors." mapSample = "The method used for calculating the MAP reading\nFor 1-2 Cylinder engines, Cycle Minimum is recommended.\nFor more than 2 cylinders Cycle Average is recommended" stoich = "The stoichiometric ration of the fuel being used. For flex fuel, choose the primary fuel" + injLayout = "The injector layout and timing to be used. Options are: \n 1. Paired - 2 injectors per output. Outputs active is equal to half the number of cylinders. Outputs are timed over 1 crank revolution. \n 2. Semi-sequential: Same as paired except that injector channels are mirrored (1&4, 2&3) meaning the number of outputs used are equal to the number of cylinders. Only valid for 4 cylinders or less. \n 3. Banked: 2 outputs only used. \n 4. Sequential: 1 injector per output and outputs used equals the number of cylinders. Injection is timed over full cycle. TrigPattern = "The type of input trigger decoder to be used." numteeth = "Number of teeth on Primary Wheel." @@ -716,8 +717,8 @@ menuDialog = main dialog = tacho, "Tacho" field = "Output pin", tachoPin - dialog = canio, "CanBus interface" - field = "Enable/Disable", canenable + dialog = canIO, "CanBus interface" + field = "Enable/Disable", canEnable dialog = accelEnrichments_center, "" field = "TPSdot Threshold", tpsThresh @@ -732,12 +733,18 @@ menuDialog = main field = "Cutoff RPM", dfcoRPM, { dfcoEnabled } field = "RPM Hysteresis", dfcoHyster, { dfcoEnabled } + dialog = accelEnrichments_north_south, "" + liveGraph = pump_ae_Graph, "AE Graph" + graphLine = afr + graphLine = TPSdot, "%", -2000, 2000, auto, auto + dialog = accelEnrichments_north, "", xAxis panel = time_accel_tpsdot_curve ;panel = time_accel_tpsdot_tbl dialog = accelEnrichments, "Acceleration Enrichment" panel = accelEnrichments_north, North + panel = accelEnrichments_north_south, Center panel = accelEnrichments_center, Center panel = accelEnrichments_south, South @@ -1189,7 +1196,7 @@ menuDialog = main gaugeCategory = "Other" clockGauge = secl, "Clock", "Seconds", 0, 255, 10, 10, 245, 245, 0, 0 deadGauge = deadValue, "---", "", 0, 1, -1, -1, 2, 2, 0, 0 - loopGauge = loopsPerSecond,"Main loop speed", "Loops/S" , 0, 20000, -1, 500,1800, 4000, 0, 0 + loopGauge = loopsPerSecond,"Main loop speed", "Loops/S" , 0, 70000, -1, 500,1800, 4000, 0, 0 memoryGauge = freeRAM, "Free memory", "bytes" , 0, 8000, -1, 1000,8000, 1000, 0, 0 ;------------------------------------------------------------------------------- @@ -1356,7 +1363,7 @@ menuDialog = main TPSdot = scalar, U08, 21, "%/s", 10.00, 0.000 advance = scalar, U08, 22, "deg", 1.000, 0.000 tps = scalar, U08, 23, "%", 1.000, 0.000 - loopsPerSecond = scalar, S16, 24, "loops", 1.000, 0.000 + loopsPerSecond = scalar, U16, 24, "loops", 1.000, 0.000 freeRAM = scalar, S16, 26, "bytes", 1.000, 0.000 batCorrection = scalar, U08, 28, "%", 1.000, 0.000 spark = scalar, U08, 29, "bits", 1.000, 0.000 diff --git a/scheduler.h b/scheduler.h index 8a9d86b..693d0c7 100644 --- a/scheduler.h +++ b/scheduler.h @@ -32,6 +32,91 @@ See page 136 of the processors datasheet: http://www.atmel.com/Images/doc2549.pd #include #endif +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) + //Refer to http://svn.savannah.nongnu.org/viewvc/trunk/avr-libc/include/avr/iomxx0_1.h?root=avr-libc&view=markup + #define FUEL1_COUNTER TCNT3 + #define FUEL2_COUNTER TCNT3 + #define FUEL3_COUNTER TCNT3 + #define FUEL4_COUNTER TCNT4 + + #define IGN1_COUNTER TCNT5 + #define IGN2_COUNTER TCNT5 + #define IGN3_COUNTER TCNT5 + #define IGN4_COUNTER TCNT4 + + #define FUEL1_COMPARE OCR3A + #define FUEL2_COMPARE OCR3B + #define FUEL3_COMPARE OCR3C + #define FUEL4_COMPARE OCR4B + + #define IGN1_COMPARE OCR5A + #define IGN2_COMPARE OCR5B + #define IGN3_COMPARE OCR5C + #define IGN4_COMPARE OCR4A + + #define FUEL1_TIMER_ENABLE() TIMSK3 |= (1 << OCIE3A) //Turn on the A compare unit (ie turn on the interrupt) + #define FUEL2_TIMER_ENABLE() TIMSK3 |= (1 << OCIE3B) //Turn on the B compare unit (ie turn on the interrupt) + #define FUEL3_TIMER_ENABLE() TIMSK3 |= (1 << OCIE3C) //Turn on the C compare unit (ie turn on the interrupt) + #define FUEL4_TIMER_ENABLE() TIMSK4 |= (1 << OCIE4B) //Turn on the B compare unit (ie turn on the interrupt) + + #define FUEL1_TIMER_DISABLE() TIMSK3 &= ~(1 << OCIE3A); //Turn off this output compare unit + #define FUEL2_TIMER_DISABLE() TIMSK3 &= ~(1 << OCIE3B); //Turn off this output compare unit + #define FUEL3_TIMER_DISABLE() TIMSK3 &= ~(1 << OCIE3C); //Turn off this output compare unit + #define FUEL4_TIMER_DISABLE() TIMSK4 &= ~(1 << OCIE4B); //Turn off this output compare unit + + #define IGN1_TIMER_ENABLE() TIMSK5 |= (1 << OCIE5A) //Turn on the A compare unit (ie turn on the interrupt) + #define IGN2_TIMER_ENABLE() TIMSK5 |= (1 << OCIE5B) //Turn on the B compare unit (ie turn on the interrupt) + #define IGN3_TIMER_ENABLE() TIMSK5 |= (1 << OCIE5C) //Turn on the C compare unit (ie turn on the interrupt) + #define IGN4_TIMER_ENABLE() TIMSK4 |= (1 << OCIE4A) //Turn on the A compare unit (ie turn on the interrupt) + + #define IGN1_TIMER_DISABLE() TIMSK5 &= ~(1 << OCIE5A) //Turn off this output compare unit + #define IGN2_TIMER_DISABLE() TIMSK5 &= ~(1 << OCIE5B) //Turn off this output compare unit + #define IGN3_TIMER_DISABLE() TIMSK5 &= ~(1 << OCIE5C) //Turn off this output compare unit + #define IGN4_TIMER_DISABLE() TIMSK4 &= ~(1 << OCIE4A) //Turn off this output compare unit + +#elif defined(CORE_TEENSY) + //http://shawnhymel.com/661/learning-the-teensy-lc-interrupt-service-routines/ + #define FUEL1_COUNTER FTM0_CNT + #define FUEL2_COUNTER FTM0_CNT + #define FUEL3_COUNTER FTM0_CNT + #define FUEL4_COUNTER FTM0_CNT + + #define IGN1_COUNTER FTM0_CNT + #define IGN2_COUNTER FTM0_CNT + #define IGN3_COUNTER FTM0_CNT + #define IGN4_COUNTER FTM0_CNT + + #define FUEL1_COMPARE FTM0_C0V + #define FUEL2_COMPARE FTM0_C1V + #define FUEL3_COMPARE FTM0_C2V + #define FUEL4_COMPARE FTM0_C3V + + #define IGN1_COMPARE FTM0_C4V + #define IGN2_COMPARE FTM0_C5V + #define IGN3_COMPARE FTM0_C6V + #define IGN4_COMPARE FTM0_C7V + + #define FUEL1_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL2_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL3_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL4_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + + #define FUEL1_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL2_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL3_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define FUEL4_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + + #define IGN1_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN2_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN3_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN4_TIMER_ENABLE() NVIC_ENABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + + #define IGN1_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN2_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN3_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! + #define IGN4_TIMER_DISABLE() NVIC_DISABLE_IRQ(IRQ_FTM1) //THIS IS NOT RIGHT! PLACEHOLDER ONLY! +#endif + void initialiseSchedulers(); void setFuelSchedule1(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()); void setFuelSchedule2(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()); @@ -63,7 +148,7 @@ struct Schedule { unsigned int endCompare; }; -Schedule *timer3Aqueue[4]; +volatile Schedule *timer3Aqueue[4]; Schedule *timer3Bqueue[4]; Schedule *timer3Cqueue[4]; @@ -86,27 +171,50 @@ Schedule ignitionSchedule8; Schedule nullSchedule; //This is placed at the end of the queue. It's status will always be set to OFF and hence will never perform any action within an ISR -static inline unsigned int setQueue(Schedule *queue[], Schedule *schedule1, Schedule *schedule2, unsigned int CNT) +static inline unsigned int setQueue(volatile Schedule *queue[], Schedule *schedule1, Schedule *schedule2, unsigned int CNT) { //Create an array of all the upcoming targets, relative to the current count on the timer unsigned int tmpQueue[4]; - tmpQueue[0] = schedule1->startCompare - CNT; - tmpQueue[1] = schedule1->endCompare - CNT; - tmpQueue[2] = schedule2->startCompare - CNT; - tmpQueue[3] = schedule2->endCompare - CNT; //Set the initial queue state. This order matches the tmpQueue order - queue[0] = schedule1; - queue[1] = schedule1; - queue[2] = schedule2; - queue[3] = schedule2; + if(schedule1->Status == OFF) + { + queue[0] = schedule2; + queue[1] = schedule2; + tmpQueue[0] = schedule2->startCompare - CNT; + tmpQueue[1] = schedule2->endCompare - CNT; + } + else + { + queue[0] = schedule1; + queue[1] = schedule1; + tmpQueue[0] = schedule1->startCompare - CNT; + tmpQueue[1] = schedule1->endCompare - CNT; + } + + if(schedule2->Status == OFF) + { + queue[2] = schedule1; + queue[3] = schedule1; + tmpQueue[2] = schedule1->startCompare - CNT; + tmpQueue[3] = schedule1->endCompare - CNT; + } + else + { + queue[2] = schedule2; + queue[3] = schedule2; + tmpQueue[2] = schedule2->startCompare - CNT; + tmpQueue[3] = schedule2->endCompare - CNT; + } + //Sort the queues. Both queues are kept in sync. - //This implementes a sorting networking based on the Bose-Nelson swap algorithm - //See: - #define SWAP(x,y) if(tmpQueue[y] < tmpQueue[x]) { unsigned int tmp = tmpQueue[x]; tmpQueue[x] = tmpQueue[y]; tmpQueue[y] = tmp; Schedule *tmpS = queue[x]; queue[x] = queue[y]; queue[y] = tmpS; } + //This implementes a sorting networking based on the Bose-Nelson sorting network + //See: http://pages.ripco.net/~jgamble/nw.html + #define SWAP(x,y) if(tmpQueue[y] < tmpQueue[x]) { unsigned int tmp = tmpQueue[x]; tmpQueue[x] = tmpQueue[y]; tmpQueue[y] = tmp; volatile Schedule *tmpS = queue[x]; queue[x] = queue[y]; queue[y] = tmpS; } //SWAP(0, 1); //Likely not needed //SWAP(2, 3); //Likely not needed + SWAP(0, 2); SWAP(1, 3); SWAP(1, 2); @@ -119,7 +227,7 @@ static inline unsigned int setQueue(Schedule *queue[], Schedule *schedule1, Sche * The current item (0) is discarded * The final queue slot is set to nullSchedule to indicate that no action should be taken */ -static inline unsigned int popQueue(Schedule *queue[]) +static inline unsigned int popQueue(volatile Schedule *queue[]) { queue[0] = queue[1]; queue[1] = queue[2]; diff --git a/scheduler.ino b/scheduler.ino index 37b22e0..28b77a8 100644 --- a/scheduler.ino +++ b/scheduler.ino @@ -10,7 +10,8 @@ A full copy of the license may be found in the projects root directory void initialiseSchedulers() { nullSchedule.Status = OFF; - + +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this // Much help in this from http://arduinomega.blogspot.com.au/2011/05/timer2-and-overflow-interrupt-lets-get.html //Fuel Schedules, which uses timer 3 TCCR3B = 0x00; //Disable Timer3 while we set it up @@ -19,13 +20,6 @@ void initialiseSchedulers() TCCR3A = 0x00; //Timer3 Control Reg A: Wave Gen Mode normal TCCR3B = (1 << CS12); //Timer3 Control Reg B: Timer Prescaler set to 256. Refer to http://www.instructables.com/files/orig/F3T/TIKL/H3WSA4V7/F3TTIKLH3WSA4V7.jpg //TCCR3B = 0x03; //Timer3 Control Reg B: Timer Prescaler set to 64. Refer to http://www.instructables.com/files/orig/F3T/TIKL/H3WSA4V7/F3TTIKLH3WSA4V7.jpg - fuelSchedule1.Status = OFF; - fuelSchedule2.Status = OFF; - fuelSchedule3.Status = OFF; - - fuelSchedule1.schedulesSet = 0; - fuelSchedule2.schedulesSet = 0; - fuelSchedule3.schedulesSet = 0; //Ignition Schedules, which uses timer 5 TCCR5B = 0x00; //Disable Timer3 while we set it up @@ -34,26 +28,40 @@ void initialiseSchedulers() TCCR5A = 0x00; //Timer5 Control Reg A: Wave Gen Mode normal //TCCR5B = (1 << CS12); //Timer5 Control Reg B: Timer Prescaler set to 256. Refer to http://www.instructables.com/files/orig/F3T/TIKL/H3WSA4V7/F3TTIKLH3WSA4V7.jpg TCCR5B = 0x03; //aka Divisor = 64 = 490.1Hz - ignitionSchedule1.Status = OFF; - ignitionSchedule2.Status = OFF; - ignitionSchedule3.Status = OFF; - ignitionSchedule1.schedulesSet = 0; - ignitionSchedule2.schedulesSet = 0; - ignitionSchedule3.schedulesSet = 0; - //The remaining Schedules (Schedules 4 for fuel and ignition) use Timer4 TCCR4B = 0x00; //Disable Timer4 while we set it up TCNT4 = 0; //Reset Timer Count TIFR4 = 0x00; //Timer4 INT Flag Reg: Clear Timer Overflow Flag TCCR4A = 0x00; //Timer4 Control Reg A: Wave Gen Mode normal TCCR4B = (1 << CS12); //Timer4 Control Reg B: aka Divisor = 256 = 122.5HzTimer Prescaler set to 256. Refer to http://www.instructables.com/files/orig/F3T/TIKL/H3WSA4V7/F3TTIKLH3WSA4V7.jpg - ignitionSchedule4.Status = OFF; - fuelSchedule4.Status = OFF; +#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +//Configure ARM timers here +#endif - ignitionSchedule4.schedulesSet = 0; + + fuelSchedule1.Status = OFF; + fuelSchedule2.Status = OFF; + fuelSchedule3.Status = OFF; + fuelSchedule4.Status = OFF; + fuelSchedule5.Status = OFF; + + fuelSchedule1.schedulesSet = 0; + fuelSchedule2.schedulesSet = 0; + fuelSchedule3.schedulesSet = 0; fuelSchedule4.schedulesSet = 0; - //Note that timer4 compare channel C is used by the idle control + fuelSchedule5.schedulesSet = 0; + + ignitionSchedule1.Status = OFF; + ignitionSchedule2.Status = OFF; + ignitionSchedule3.Status = OFF; + ignitionSchedule4.Status = OFF; + + ignitionSchedule1.schedulesSet = 0; + ignitionSchedule2.schedulesSet = 0; + ignitionSchedule3.schedulesSet = 0; + ignitionSchedule4.schedulesSet = 0; + } /* @@ -81,14 +89,14 @@ void setFuelSchedule1(void (*startCallback)(), unsigned long timeout, unsigned l * unsigned int absoluteTimeout = TCNT3 + (timeout / 16); //Each tick occurs every 16uS with the 256 prescaler, so divide the timeout by 16 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as required */ noInterrupts(); - fuelSchedule1.startCompare = TCNT3 + (timeout >> 4); //As above, but with bit shift instead of / 16 + fuelSchedule1.startCompare = FUEL1_COUNTER + (timeout >> 4); //As above, but with bit shift instead of / 16 fuelSchedule1.endCompare = fuelSchedule1.startCompare + (duration >> 4); fuelSchedule1.Status = PENDING; //Turn this schedule on fuelSchedule1.schedulesSet++; //Increment the number of times this schedule has been set - if(channel5InjEnabled) { OCR3A = setQueue(timer3Aqueue, &fuelSchedule1, &fuelSchedule5, TCNT3); } //Schedule 1 shares a timer with schedule 5 - else { timer3Aqueue[0] = &fuelSchedule1; timer3Aqueue[1] = &fuelSchedule1; timer3Aqueue[2] = &fuelSchedule1; timer3Aqueue[3] = &fuelSchedule1; OCR3A = fuelSchedule1.startCompare; } + if(channel5InjEnabled) { FUEL1_COMPARE = setQueue(timer3Aqueue, &fuelSchedule1, &fuelSchedule5, FUEL1_COUNTER); } //Schedule 1 shares a timer with schedule 5 + else { timer3Aqueue[0] = &fuelSchedule1; timer3Aqueue[1] = &fuelSchedule1; timer3Aqueue[2] = &fuelSchedule1; timer3Aqueue[3] = &fuelSchedule1; FUEL1_COMPARE = fuelSchedule1.startCompare; } interrupts(); - TIMSK3 |= (1 << OCIE3A); //Turn on the A compare unit (ie turn on the interrupt) + FUEL1_TIMER_ENABLE(); } void setFuelSchedule2(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { @@ -105,47 +113,59 @@ void setFuelSchedule2(void (*startCallback)(), unsigned long timeout, unsigned l * unsigned int absoluteTimeout = TCNT3 + (timeout / 16); //Each tick occurs every 16uS with the 256 prescaler, so divide the timeout by 16 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as required */ noInterrupts(); - fuelSchedule2.startCompare = TCNT3 + (timeout >> 4); //As above, but with bit shift instead of / 16 + fuelSchedule2.startCompare = FUEL2_COUNTER + (timeout >> 4); //As above, but with bit shift instead of / 16 fuelSchedule2.endCompare = fuelSchedule2.startCompare + (duration >> 4); - OCR3B = fuelSchedule2.startCompare; //Use the B copmare unit of timer 3 + FUEL2_COMPARE = fuelSchedule2.startCompare; //Use the B copmare unit of timer 3 fuelSchedule2.Status = PENDING; //Turn this schedule on fuelSchedule2.schedulesSet++; //Increment the number of times this schedule has been set interrupts(); - TIMSK3 |= (1 << OCIE3B); //Turn on the B compare unit (ie turn on the interrupt) + FUEL2_TIMER_ENABLE(); } void setFuelSchedule3(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { if(fuelSchedule3.Status == RUNNING) { return; } //Check that we're not already part way through a schedule - - //We need to calculate the value to reset the timer to (preload) in order to achieve the desired overflow time - //As the timer is ticking every 16uS (Time per Tick = (Prescale)*(1/Frequency)) - //unsigned int absoluteTimeout = TCNT3 + (timeout / 16); //Each tick occurs every 16uS with the 256 prescaler, so divide the timeout by 16 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as require - fuelSchedule3.startCompare = TCNT3 + (timeout >> 4); //As above, but with bit shift instead of / 16 - fuelSchedule3.endCompare = fuelSchedule3.startCompare + (duration >> 4); - OCR3C = fuelSchedule3.startCompare; //Use the C copmare unit of timer 3 - fuelSchedule3.duration = duration; + fuelSchedule3.StartCallback = startCallback; //Name the start callback function fuelSchedule3.EndCallback = endCallback; //Name the end callback function + fuelSchedule3.duration = duration; + + /* + * The following must be enclosed in the noIntterupts block to avoid contention caused if the relevant interrupts fires before the state is fully set + * We need to calculate the value to reset the timer to (preload) in order to achieve the desired overflow time + * As the timer is ticking every 16uS (Time per Tick = (Prescale)*(1/Frequency)) + * unsigned int absoluteTimeout = TCNT3 + (timeout / 16); //Each tick occurs every 16uS with the 256 prescaler, so divide the timeout by 16 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as required + */ + noInterrupts(); + fuelSchedule3.startCompare = FUEL3_COUNTER + (timeout >> 4); //As above, but with bit shift instead of / 16 + fuelSchedule3.endCompare = fuelSchedule3.startCompare + (duration >> 4); + FUEL3_COMPARE = fuelSchedule3.startCompare; //Use the C copmare unit of timer 3 fuelSchedule3.Status = PENDING; //Turn this schedule on fuelSchedule3.schedulesSet++; //Increment the number of times this schedule has been set - TIMSK3 |= (1 << OCIE3C); //Turn on the C compare unit (ie turn on the interrupt) + interrupts(); + FUEL3_TIMER_ENABLE(); } void setFuelSchedule4(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) //Uses timer 4 compare B { if(fuelSchedule4.Status == RUNNING) { return; } //Check that we're not already part way through a schedule - - //We need to calculate the value to reset the timer to (preload) in order to achieve the desired overflow time - //As the timer is ticking every 16uS (Time per Tick = (Prescale)*(1/Frequency)) - //unsigned int absoluteTimeout = TCNT4 + (timeout / 4); //Each tick occurs every 4uS with the 128 prescaler, so divide the timeout by 4 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as required - fuelSchedule4.startCompare = TCNT4 + (timeout >> 4); - fuelSchedule4.endCompare = fuelSchedule4.startCompare + (duration >> 4); - OCR4B = fuelSchedule4.startCompare; //Use the C copmare unit of timer 3 - fuelSchedule4.duration = duration; + fuelSchedule4.StartCallback = startCallback; //Name the start callback function fuelSchedule4.EndCallback = endCallback; //Name the end callback function + fuelSchedule4.duration = duration; + + /* + * The following must be enclosed in the noIntterupts block to avoid contention caused if the relevant interrupts fires before the state is fully set + * We need to calculate the value to reset the timer to (preload) in order to achieve the desired overflow time + * As the timer is ticking every 16uS (Time per Tick = (Prescale)*(1/Frequency)) + * unsigned int absoluteTimeout = TCNT3 + (timeout / 16); //Each tick occurs every 16uS with the 256 prescaler, so divide the timeout by 16 to get ther required number of ticks. Add this to the current tick count to get the target time. This will automatically overflow as required + */ + noInterrupts(); + fuelSchedule4.startCompare = FUEL4_COUNTER + (timeout >> 4); + fuelSchedule4.endCompare = fuelSchedule4.startCompare + (duration >> 4); + FUEL4_COMPARE = fuelSchedule4.startCompare; //Use the C copmare unit of timer 3 fuelSchedule4.Status = PENDING; //Turn this schedule on fuelSchedule4.schedulesSet++; //Increment the number of times this schedule has been set - TIMSK4 |= (1 << OCIE4B); //Turn on the B compare unit (ie turn on the interrupt) + interrupts(); + FUEL4_TIMER_ENABLE(); } void setFuelSchedule5(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { @@ -161,6 +181,7 @@ void setFuelSchedule5(void (*startCallback)(), unsigned long timeout, unsigned l /* * The following must be enclosed in the noIntterupts block to avoid contention caused if the relevant interrupts fires before the state is fully set */ +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) noInterrupts(); fuelSchedule5.startCompare = TCNT3 + (timeout >> 4); //As above, but with bit shift instead of / 16 fuelSchedule5.endCompare = fuelSchedule5.startCompare + (duration >> 4); @@ -169,80 +190,103 @@ void setFuelSchedule5(void (*startCallback)(), unsigned long timeout, unsigned l OCR3A = setQueue(timer3Aqueue, &fuelSchedule1, &fuelSchedule5, TCNT3); //Schedule 1 shares a timer with schedule 5 interrupts(); TIMSK3 |= (1 << OCIE3A); //Turn on the A compare unit (ie turn on the interrupt) +#endif } //Ignition schedulers use Timer 5 void setIgnitionSchedule1(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { if(ignitionSchedule1.Status == RUNNING) { return; } //Check that we're not already part way through a schedule + + ignitionSchedule1.StartCallback = startCallback; //Name the start callback function + ignitionSchedule1.EndCallback = endCallback; //Name the start callback function + ignitionSchedule1.duration = duration; //As the timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when appliedcausing erratic behaviour such as erroneous sparking. - OCR5A = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) - ignitionSchedule1.duration = duration; - ignitionSchedule1.StartCallback = startCallback; //Name the start callback function - ignitionSchedule1.EndCallback = endCallback; //Name the start callback function + noInterrupts(); + ignitionSchedule1.startCompare = IGN1_COUNTER + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) + ignitionSchedule1.endCompare = ignitionSchedule1.startCompare + (duration >> 2); + IGN1_COMPARE = ignitionSchedule1.startCompare; ignitionSchedule1.Status = PENDING; //Turn this schedule on - TIMSK5 |= (1 << OCIE5A); //Turn on the A compare unit (ie turn on the interrupt) + interrupts(); + IGN1_TIMER_ENABLE(); } void setIgnitionSchedule2(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { if(ignitionSchedule2.Status == RUNNING) { return; } //Check that we're not already part way through a schedule - //As the timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) - if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when applied causing erratic behaviour such as erroneous sparking. This must be set slightly lower than the max of 262140 to avoid strangeness - OCR5B = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) - - ignitionSchedule2.duration = duration; ignitionSchedule2.StartCallback = startCallback; //Name the start callback function ignitionSchedule2.EndCallback = endCallback; //Name the start callback function + ignitionSchedule2.duration = duration; + + //As the timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) + if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when applied causing erratic behaviour such as erroneous sparking. This must be set slightly lower than the max of 262140 to avoid strangeness + + noInterrupts(); + ignitionSchedule2.startCompare = IGN2_COUNTER + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) + ignitionSchedule2.endCompare = ignitionSchedule2.startCompare + (duration >> 2); + IGN2_COMPARE = ignitionSchedule2.startCompare; ignitionSchedule2.Status = PENDING; //Turn this schedule on - TIMSK5 |= (1 << OCIE5B); //Turn on the B compare unit (ie turn on the interrupt) + interrupts(); + IGN1_TIMER_ENABLE(); } void setIgnitionSchedule3(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { if(ignitionSchedule3.Status == RUNNING) { return; } //Check that we're not already part way through a schedule + + ignitionSchedule3.StartCallback = startCallback; //Name the start callback function + ignitionSchedule3.EndCallback = endCallback; //Name the start callback function + ignitionSchedule3.duration = duration; //The timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when applied causing erratic behaviour such as erroneous sparking. This must be set slightly lower than the max of 262140 to avoid strangeness - OCR5C = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) - ignitionSchedule3.duration = duration; - ignitionSchedule3.StartCallback = startCallback; //Name the start callback function - ignitionSchedule3.EndCallback = endCallback; //Name the start callback function + noInterrupts(); + ignitionSchedule3.startCompare = IGN3_COUNTER + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) + ignitionSchedule3.endCompare = ignitionSchedule3.startCompare + (duration >> 2); + IGN3_COMPARE = ignitionSchedule3.startCompare; ignitionSchedule3.Status = PENDING; //Turn this schedule on - TIMSK5 |= (1 << OCIE5C); //Turn on the C compare unit (ie turn on the interrupt) + interrupts(); + IGN3_TIMER_ENABLE(); } void setIgnitionSchedule4(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { if(ignitionSchedule4.Status == RUNNING) { return; } //Check that we're not already part way through a schedule + + ignitionSchedule4.StartCallback = startCallback; //Name the start callback function + ignitionSchedule4.EndCallback = endCallback; //Name the start callback function + ignitionSchedule4.duration = duration; //We need to calculate the value to reset the timer to (preload) in order to achieve the desired overflow time //The timer is ticking every 16uS (Time per Tick = (Prescale)*(1/Frequency)) //Note this is different to the other ignition timers - unsigned int absoluteTimeout = TCNT4 + (timeout >> 4); //As above, but with bit shift instead of / 16 - - OCR4A = absoluteTimeout; - ignitionSchedule4.duration = duration; - ignitionSchedule4.StartCallback = startCallback; //Name the start callback function - ignitionSchedule4.EndCallback = endCallback; //Name the start callback function + + noInterrupts(); + ignitionSchedule4.startCompare = IGN4_COUNTER + (timeout >> 4); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) + ignitionSchedule4.endCompare = ignitionSchedule4.startCompare + (duration >> 4); + IGN4_COMPARE = ignitionSchedule4.startCompare; ignitionSchedule4.Status = PENDING; //Turn this schedule on - TIMSK4 |= (1 << OCIE4A); //Turn on the A compare unit (ie turn on the interrupt) + interrupts(); + IGN4_TIMER_ENABLE(); } void setIgnitionSchedule5(void (*startCallback)(), unsigned long timeout, unsigned long duration, void(*endCallback)()) { return; if(ignitionSchedule1.Status == RUNNING) { return; } //Check that we're not already part way through a schedule + + ignitionSchedule5.StartCallback = startCallback; //Name the start callback function + ignitionSchedule5.EndCallback = endCallback; //Name the start callback function + ignitionSchedule5.duration = duration; //As the timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when applied causing erratic behaviour such as erroneous sparking. This must be set slightly lower than the max of 262140 to avoid strangeness - OCR5A = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) - ignitionSchedule5.duration = duration; - ignitionSchedule5.StartCallback = startCallback; //Name the start callback function - ignitionSchedule5.EndCallback = endCallback; //Name the start callback function +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) + OCR5A = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) ignitionSchedule5.Status = PENDING; //Turn this schedule on TIMSK5 |= (1 << OCIE5A); //Turn on the A compare unit (ie turn on the interrupt) +#endif } /*******************************************************************************************************************************************************************************************************/ @@ -250,44 +294,52 @@ void setIgnitionSchedule5(void (*startCallback)(), unsigned long timeout, unsign //This calls the relevant callback function (startCallback or endCallback) depending on the status of the schedule. //If the startCallback function is called, we put the scheduler into RUNNING state //Timer3A (fuel schedule 1) Compare Vector +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER3_COMPA_vect, ISR_NOBLOCK) //fuelSchedules 1 and 5 +#elif defined (CORE_TEENSY) +void timer3compareAinterrupt() //Most ARM chips can simply call a function +#endif { - if (timer3Aqueue[0]->Status == OFF) { TIMSK3 &= ~(1 << OCIE3A); return; } //Safety check. Turn off this output compare unit and return without performing any action + if (timer3Aqueue[0]->Status == OFF) { FUEL1_TIMER_DISABLE(); return; } //Safety check. Turn off this output compare unit and return without performing any action if (timer3Aqueue[0]->Status == PENDING) //Check to see if this schedule is turn on { timer3Aqueue[0]->StartCallback(); timer3Aqueue[0]->Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) - OCR3A = popQueue(timer3Aqueue); + FUEL1_COMPARE = popQueue(timer3Aqueue); } else if (timer3Aqueue[0]->Status == RUNNING) { timer3Aqueue[0]->EndCallback(); timer3Aqueue[0]->Status = OFF; //Turn off the schedule timer3Aqueue[0]->schedulesSet = 0; - OCR3A = popQueue(timer3Aqueue); + FUEL1_COMPARE = popQueue(timer3Aqueue); } } +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER3_COMPB_vect, ISR_NOBLOCK) //fuelSchedule2 +#elif defined (CORE_TEENSY) +void timer3compareBinterrupt() //Most ARM chips can simply call a function +#endif { if (fuelSchedule2.Status == PENDING) //Check to see if this schedule is turn on { fuelSchedule2.StartCallback(); fuelSchedule2.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) - OCR3B = fuelSchedule2.endCompare; + FUEL2_COMPARE = fuelSchedule2.endCompare; } else if (fuelSchedule2.Status == RUNNING) { fuelSchedule2.EndCallback(); fuelSchedule2.Status = OFF; //Turn off the schedule fuelSchedule2.schedulesSet = 0; - TIMSK3 &= ~(1 << OCIE3B); //Turn off this output compare unit (This simply writes 0 to the OCIE3A bit of TIMSK3) + FUEL2_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER3_COMPC_vect, ISR_NOBLOCK) //fuelSchedule3 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer3compareCinterrupt() //Most ARM chips can simply call a function #endif { @@ -295,20 +347,20 @@ void timer3compareCinterrupt() //Most ARM chips can simply call a function { fuelSchedule3.StartCallback(); fuelSchedule3.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) - OCR3C = fuelSchedule3.endCompare; + FUEL3_COMPARE = fuelSchedule3.endCompare; } else if (fuelSchedule3.Status == RUNNING) { fuelSchedule3.EndCallback(); fuelSchedule3.Status = OFF; //Turn off the schedule fuelSchedule3.schedulesSet = 0; - TIMSK3 &= ~(1 << OCIE3C); //Turn off this output compare unit (This simply writes 0 to the OCIE3A bit of TIMSK3) + FUEL3_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER4_COMPB_vect, ISR_NOBLOCK) //fuelSchedule4 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer4compareBinterrupt() //Most ARM chips can simply call a function #endif { @@ -316,109 +368,105 @@ void timer4compareBinterrupt() //Most ARM chips can simply call a function { fuelSchedule4.StartCallback(); fuelSchedule4.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) - OCR4B = fuelSchedule4.endCompare; + FUEL4_COMPARE = fuelSchedule4.endCompare; } else if (fuelSchedule4.Status == RUNNING) { fuelSchedule4.EndCallback(); fuelSchedule4.Status = OFF; //Turn off the schedule fuelSchedule4.schedulesSet = 0; - TIMSK4 &= ~(1 << OCIE4B); //Turn off this output compare unit (This simply writes 0 to the OCIE3A bit of TIMSK3) + FUEL4_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER5_COMPA_vect, ISR_NOBLOCK) //ignitionSchedule1 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer5compareAinterrupt() //Most ARM chips can simply call a function #endif { if (ignitionSchedule1.Status == PENDING) //Check to see if this schedule is turn on { - //if ( ign1LastRev == startRevolutions ) { return; } + ignitionSchedule1.StartCallback(); ignitionSchedule1.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) ignitionSchedule1.startTime = micros(); - ignitionSchedule1.StartCallback(); ign1LastRev = startRevolutions; - OCR5A = TCNT5 + (ignitionSchedule1.duration >> 2); //Divide by 4 + IGN1_COMPARE = ignitionSchedule1.endCompare; //OCR5A = TCNT5 + (ignitionSchedule1.duration >> 2); //Divide by 4 } else if (ignitionSchedule1.Status == RUNNING) { ignitionSchedule1.Status = OFF; //Turn off the schedule ignitionSchedule1.EndCallback(); ignitionCount += 1; //Increment the igintion counter - TIMSK5 &= ~(1 << OCIE5A); //Turn off this output compare unit + IGN1_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER5_COMPB_vect, ISR_NOBLOCK) //ignitionSchedule2 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer5compareBinterrupt() //Most ARM chips can simply call a function #endif { if (ignitionSchedule2.Status == PENDING) //Check to see if this schedule is turn on { - //if ( ign2LastRev == startRevolutions ) { return; } + ignitionSchedule2.StartCallback(); ignitionSchedule2.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) ignitionSchedule2.startTime = micros(); - ignitionSchedule2.StartCallback(); ign2LastRev = startRevolutions; - OCR5B = TCNT5 + (ignitionSchedule2.duration >> 2); + IGN2_COMPARE = ignitionSchedule2.endCompare; //OCR5B = TCNT5 + (ignitionSchedule2.duration >> 2); } else if (ignitionSchedule2.Status == RUNNING) { ignitionSchedule2.Status = OFF; //Turn off the schedule ignitionSchedule2.EndCallback(); ignitionCount += 1; //Increment the igintion counter - TIMSK5 &= ~(1 << OCIE5B); //Turn off this output compare unit (This simply writes 0 to the OCIE3A bit of TIMSK3) + IGN2_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER5_COMPC_vect, ISR_NOBLOCK) //ignitionSchedule3 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer5compareCinterrupt() //Most ARM chips can simply call a function #endif { if (ignitionSchedule3.Status == PENDING) //Check to see if this schedule is turn on { - //if ( ign3LastRev == startRevolutions ) { return; } + ignitionSchedule3.StartCallback(); ignitionSchedule3.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) ignitionSchedule3.startTime = micros(); - ignitionSchedule3.StartCallback(); ign3LastRev = startRevolutions; - OCR5C = TCNT5 + (ignitionSchedule3.duration >> 2); + IGN3_COMPARE = ignitionSchedule3.endCompare; //OCR5C = TCNT5 + (ignitionSchedule3.duration >> 2); } else if (ignitionSchedule3.Status == RUNNING) { ignitionSchedule3.Status = OFF; //Turn off the schedule ignitionSchedule3.EndCallback(); ignitionCount += 1; //Increment the igintion counter - TIMSK5 &= ~(1 << OCIE5C); //Turn off this output compare unit (This simply writes 0 to the OCIE3A bit of TIMSK3) + IGN3_TIMER_DISABLE(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this ISR(TIMER4_COMPA_vect, ISR_NOBLOCK) //ignitionSchedule4 -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (CORE_TEENSY) void timer4compareAinterrupt() //Most ARM chips can simply call a function #endif { if (ignitionSchedule4.Status == PENDING) //Check to see if this schedule is turn on { - //if ( ign4LastRev == startRevolutions ) { return; } + ignitionSchedule4.StartCallback(); ignitionSchedule4.Status = RUNNING; //Set the status to be in progress (ie The start callback has been called, but not the end callback) ignitionSchedule4.startTime = micros(); - ignitionSchedule4.StartCallback(); ign4LastRev = startRevolutions; - OCR4A = TCNT4 + (ignitionSchedule4.duration >> 4); //Divide by 16 + IGN4_COMPARE = ignitionSchedule4.endCompare; //OCR4A = TCNT4 + (ignitionSchedule4.duration >> 4); //Divide by 16 } else if (ignitionSchedule4.Status == RUNNING) { ignitionSchedule4.Status = OFF; //Turn off the schedule ignitionSchedule4.EndCallback(); ignitionCount += 1; //Increment the igintion counter - TIMSK4 &= ~(1 << OCIE4A); //Turn off this output compare unit (This simply writes 0 to the OCIE4A bit of TIMSK4) + IGN4_TIMER_DISABLE(); } } diff --git a/speeduino.ino b/speeduino.ino index 56fbdb4..0f56e74 100644 --- a/speeduino.ino +++ b/speeduino.ino @@ -97,9 +97,10 @@ unsigned long MAPrunningValue; //Used for tracking either the total of all MAP r unsigned int MAPcount; //Number of samples taken in the current MAP cycle byte MAPcurRev = 0; //Tracks which revolution we're sampling on -int CRANK_ANGLE_MAX = 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) +int CRANK_ANGLE_MAX = 720; +int CRANK_ANGLE_MAX_IGN, 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) static byte coilHIGH = HIGH; static byte coilLOW = LOW; @@ -149,7 +150,10 @@ volatile bool fpPrimed = false; //Tracks whether or not the fuel pump priming ha void setup() { - Serial.begin(115200); + Serial.begin(115200); +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //ATmega2561 does not have Serial3 + if (configPage1.canEnable) { Serial3.begin(115200); } +#endif //Setup the dummy fuel and ignition tables //dummyFuelTable(&fuelTable); @@ -161,7 +165,6 @@ void setup() table3D_setSize(&vvtTable, 8); loadConfig(); - if (configPage1.canenable ==1){Serial3.begin(115200);} //Repoint the 2D table structs to the config pages that were just loaded taeTable.valueSize = SIZE_BYTE; //Set this table to use byte values @@ -457,7 +460,7 @@ void setup() currentLoopTime = micros(); - +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this //This sets the ADC (Analog to Digitial Converter) to run at 1Mhz, greatly reducing analog read times (MAP/TPS) //1Mhz is the fastest speed permitted by the CPU without affecting accuracy //Please see chapter 11 of 'Practical Arduino' (http://books.google.com.au/books?id=HsTxON1L6D4C&printsec=frontcover#v=onepage&q&f=false) for more details @@ -467,6 +470,7 @@ void setup() cbi(ADCSRA,ADPS1); cbi(ADCSRA,ADPS0); #endif +#endif mainLoopCount = 0; @@ -483,11 +487,14 @@ void setup() case 2: channel1IgnDegrees = 0; - if (configPage1.engineType == EVEN_FIRE ) { channel2IgnDegrees = 180; } + if (configPage1.engineType == EVEN_FIRE ) + { + channel2IgnDegrees = 180; + } else { channel2IgnDegrees = configPage1.oddfire2; } - - //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL) + + //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) @@ -512,17 +519,19 @@ void setup() } //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL) + if(configPage1.injLayout == INJ_SEMISEQUENTIAL) { channel1InjDegrees = 0; channel2InjDegrees = channel2IgnDegrees; channel3InjDegrees = channel2IgnDegrees; } - else if (configPage1.injTiming == INJ_SEQUENTIAL) + else if (configPage1.injLayout == INJ_SEQUENTIAL) { channel1InjDegrees = 0; channel2InjDegrees = 240; channel3InjDegrees = 480; + CRANK_ANGLE_MAX_INJ = 720; + req_fuel_uS = req_fuel_uS * 2; } else { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = 0; } //For simultaneous, all squirts happen at the same time @@ -537,10 +546,12 @@ void setup() { channel2IgnDegrees = 180; - if(useSequentialIgnition) + if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL) { channel3IgnDegrees = 360; channel4IgnDegrees = 540; + + CRANK_ANGLE_MAX_IGN = 720; } } else @@ -551,17 +562,23 @@ void setup() } //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL) + if(configPage1.injLayout == INJ_SEMISEQUENTIAL) { channel1InjDegrees = 0; channel2InjDegrees = channel2IgnDegrees; } - else if (useSequentialFuel) + else if (configPage1.injLayout == INJ_SEQUENTIAL) { channel1InjDegrees = 0; - channel2InjDegrees = channel2IgnDegrees; - channel3InjDegrees = channel3IgnDegrees; - channel4InjDegrees = channel4IgnDegrees; + channel2InjDegrees = 180; + channel3InjDegrees = 360; + channel4InjDegrees = 540; + + channel3InjEnabled = true; + channel4InjEnabled = true; + + CRANK_ANGLE_MAX_INJ = 720; + req_fuel_uS = req_fuel_uS * 2; } else { channel1InjDegrees = channel2InjDegrees = 0; } //For simultaneous, all squirts happen at the same time @@ -575,16 +592,18 @@ void setup() channel4IgnDegrees = 216; channel5IgnDegrees = 288; - if(useSequentialIgnition) + if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL) { channel2IgnDegrees = 144; channel3IgnDegrees = 288; channel4IgnDegrees = 432; channel5IgnDegrees = 576; + + CRANK_ANGLE_MAX_IGN = 720; } //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL) + if(configPage1.injLayout == INJ_SEMISEQUENTIAL) { channel1InjDegrees = 0; channel2InjDegrees = 72; @@ -592,13 +611,15 @@ void setup() channel4InjDegrees = 216; channel5InjDegrees = 288; } - else if (useSequentialFuel) + else if (configPage1.injLayout == INJ_SEQUENTIAL) { channel1InjDegrees = 0; channel2InjDegrees = 144; channel3InjDegrees = 288; channel4InjDegrees = 432; channel5InjDegrees = 576; + + CRANK_ANGLE_MAX_INJ = 720; } else { channel1InjDegrees = channel2InjDegrees = channel3InjDegrees = channel4InjDegrees = channel5InjDegrees = 0; } //For simultaneous, all squirts happen at the same time @@ -614,7 +635,7 @@ void setup() channel3IgnDegrees = 240; //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL || configPage1.injTiming == INJ_SEQUENTIAL) //No full sequential for more than 4 cylinders + if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injLayout == INJ_SEQUENTIAL) //No full sequential for more than 4 cylinders { channel1InjDegrees = 0; channel2InjDegrees = 120; @@ -635,7 +656,7 @@ void setup() channel4IgnDegrees = 270; //For alternatiing injection, the squirt occurs at different times for each channel - if(configPage1.injTiming == INJ_SEMISEQUENTIAL || configPage1.injTiming == INJ_SEQUENTIAL) //No full sequential for more than 4 cylinders + if(configPage1.injLayout == INJ_SEMISEQUENTIAL || configPage1.injTiming == INJ_SEQUENTIAL) //No full sequential for more than 4 cylinders { channel1InjDegrees = 0; channel2InjDegrees = 90; @@ -754,17 +775,19 @@ void loop() command(); } } +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //ATmega2561 does not have Serial3 //if Can interface is enabled then check for serial3 requests. - if (configPage1.canenable == 1) + if (configPage1.canEnable) { if ( ((mainLoopCount & 31) == 1) or (Serial3.available() > SERIAL_BUFFER_THRESHOLD) ) { if (Serial3.available() > 0) { - Cancommand(); + canCommand(); } } } +#endif // if (configPage1.displayType && (mainLoopCount & 255) == 1) { updateDisplay();} //Displays currently disabled @@ -795,7 +818,7 @@ void loop() fuelOn = false; if (fpPrimed) { digitalWrite(pinFuelPump, LOW); } //Turn off the fuel pump, but only if the priming is complete fuelPumpOn = false; - TIMSK4 &= ~(1 << OCIE4C); digitalWrite(pinIdle1, LOW); //Turns off the idle control PWM. This REALLY needs to be cleaned up into a general PWM controller class + disableIdle(); //Turn off the idle PWM } //Uncomment the following for testing @@ -989,53 +1012,66 @@ void loop() //Determine next firing angles int PWdivTimerPerDegree = div(currentStatus.PW, 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 - if(injector1StartAngle < 0) {injector1StartAngle += CRANK_ANGLE_MAX;} + if(injector1StartAngle < 0) {injector1StartAngle += CRANK_ANGLE_MAX_INJ;} //Repeat the above for each cylinder switch (configPage1.nCylinders) { //2 cylinders case 2: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} break; //3 cylinders case 3: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} injector3StartAngle = (configPage1.inj3Ang + channel3InjDegrees - ( PWdivTimerPerDegree )); - if(injector3StartAngle > CRANK_ANGLE_MAX) {injector3StartAngle -= CRANK_ANGLE_MAX;} + if(injector3StartAngle > CRANK_ANGLE_MAX_INJ) {injector3StartAngle -= CRANK_ANGLE_MAX_INJ;} break; //4 cylinders case 4: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} + + if(configPage1.injLayout == INJ_SEQUENTIAL) + { + injector3StartAngle = (configPage1.inj3Ang + channel3InjDegrees - ( PWdivTimerPerDegree )); + if(injector3StartAngle > CRANK_ANGLE_MAX_INJ) {injector3StartAngle -= CRANK_ANGLE_MAX_INJ;} + injector4StartAngle = (configPage1.inj4Ang + channel4InjDegrees - ( PWdivTimerPerDegree )); + if(injector4StartAngle > CRANK_ANGLE_MAX_INJ) {injector4StartAngle -= CRANK_ANGLE_MAX_INJ;} + + injector1StartAngle += 360; + injector2StartAngle += 360; + injector3StartAngle += 360; + injector4StartAngle += 360; + } break; //5 cylinders case 5: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} injector3StartAngle = (configPage1.inj3Ang + channel3InjDegrees - ( PWdivTimerPerDegree )); - if(injector3StartAngle > CRANK_ANGLE_MAX) {injector3StartAngle -= CRANK_ANGLE_MAX;} + if(injector3StartAngle > CRANK_ANGLE_MAX_INJ) {injector3StartAngle -= CRANK_ANGLE_MAX_INJ;} injector4StartAngle = (configPage1.inj4Ang + channel4InjDegrees - ( PWdivTimerPerDegree )); - if(injector4StartAngle > CRANK_ANGLE_MAX) {injector4StartAngle -= CRANK_ANGLE_MAX;} + if(injector4StartAngle > CRANK_ANGLE_MAX_INJ) {injector4StartAngle -= CRANK_ANGLE_MAX_INJ;} injector5StartAngle = (configPage1.inj1Ang + channel5InjDegrees - ( PWdivTimerPerDegree )); - if(injector5StartAngle > CRANK_ANGLE_MAX) {injector5StartAngle -= CRANK_ANGLE_MAX;} + if(injector5StartAngle > CRANK_ANGLE_MAX_INJ) {injector5StartAngle -= CRANK_ANGLE_MAX_INJ;} break; //6 cylinders case 6: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} injector3StartAngle = (configPage1.inj3Ang + channel3InjDegrees - ( PWdivTimerPerDegree )); - if(injector3StartAngle > CRANK_ANGLE_MAX) {injector3StartAngle -= CRANK_ANGLE_MAX;} + if(injector3StartAngle > CRANK_ANGLE_MAX_INJ) {injector3StartAngle -= CRANK_ANGLE_MAX_INJ;} break; //8 cylinders case 8: injector2StartAngle = (configPage1.inj2Ang + channel2InjDegrees - ( PWdivTimerPerDegree )); - if(injector2StartAngle > CRANK_ANGLE_MAX) {injector2StartAngle -= CRANK_ANGLE_MAX;} + if(injector2StartAngle > CRANK_ANGLE_MAX_INJ) {injector2StartAngle -= CRANK_ANGLE_MAX_INJ;} injector3StartAngle = (configPage1.inj3Ang + channel3InjDegrees - ( PWdivTimerPerDegree )); - if(injector3StartAngle > CRANK_ANGLE_MAX) {injector3StartAngle -= CRANK_ANGLE_MAX;} + if(injector3StartAngle > CRANK_ANGLE_MAX_INJ) {injector3StartAngle -= CRANK_ANGLE_MAX_INJ;} injector4StartAngle = (configPage1.inj4Ang + channel4InjDegrees - ( PWdivTimerPerDegree )); - if(injector4StartAngle > CRANK_ANGLE_MAX) {injector4StartAngle -= CRANK_ANGLE_MAX;} + if(injector4StartAngle > CRANK_ANGLE_MAX_INJ) {injector4StartAngle -= CRANK_ANGLE_MAX_INJ;} break; //Will hit the default case on 1 cylinder or >8 cylinders. Do nothing in these cases default: @@ -1061,45 +1097,53 @@ void loop() //Calculate start angle for each channel //1 cylinder (Everyone gets this) - ignition1StartAngle = CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; // 360 - desired advance angle - number of degrees the dwell will take - if(ignition1StartAngle < 0) {ignition1StartAngle += CRANK_ANGLE_MAX;} + ignition1StartAngle = CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; // 360 - desired advance angle - number of degrees the dwell will take + if(ignition1StartAngle < 0) {ignition1StartAngle += CRANK_ANGLE_MAX_IGN;} //This test for more cylinders and do the same thing switch (configPage1.nCylinders) { //2 cylinders case 2: - ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition2StartAngle > CRANK_ANGLE_MAX) {ignition2StartAngle -= CRANK_ANGLE_MAX;} + ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} break; //3 cylinders case 3: - ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition2StartAngle > CRANK_ANGLE_MAX) {ignition2StartAngle -= CRANK_ANGLE_MAX;} + ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} ignition3StartAngle = channel3IgnDegrees + 360 - currentStatus.advance - dwellAngle; - if(ignition3StartAngle > CRANK_ANGLE_MAX) {ignition3StartAngle -= CRANK_ANGLE_MAX;} + if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;} break; //4 cylinders case 4: - ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition2StartAngle > CRANK_ANGLE_MAX) {ignition2StartAngle -= CRANK_ANGLE_MAX;} - if(ignition2StartAngle < 0) {ignition2StartAngle += CRANK_ANGLE_MAX;} + ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} + if(ignition2StartAngle < 0) {ignition2StartAngle += CRANK_ANGLE_MAX_IGN;} + + if(configPage2.sparkMode == IGN_MODE_SEQUENTIAL) + { + ignition3StartAngle = channel3IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;} + ignition4StartAngle = channel4IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; + if(ignition4StartAngle > CRANK_ANGLE_MAX_IGN) {ignition4StartAngle -= CRANK_ANGLE_MAX_IGN;} + } break; //6 cylinders case 6: - ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition2StartAngle > CRANK_ANGLE_MAX) {ignition2StartAngle -= CRANK_ANGLE_MAX;} - ignition3StartAngle = channel3IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition3StartAngle > CRANK_ANGLE_MAX) {ignition3StartAngle -= CRANK_ANGLE_MAX;} + ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} + ignition3StartAngle = channel3IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;} break; //8 cylinders case 8: - ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition2StartAngle > CRANK_ANGLE_MAX) {ignition2StartAngle -= CRANK_ANGLE_MAX;} + ignition2StartAngle = channel2IgnDegrees + CRANK_ANGLE_MAX_IGN - currentStatus.advance - dwellAngle; + if(ignition2StartAngle > CRANK_ANGLE_MAX_IGN) {ignition2StartAngle -= CRANK_ANGLE_MAX_IGN;} ignition3StartAngle = channel3IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition3StartAngle > CRANK_ANGLE_MAX) {ignition3StartAngle -= CRANK_ANGLE_MAX;} + if(ignition3StartAngle > CRANK_ANGLE_MAX_IGN) {ignition3StartAngle -= CRANK_ANGLE_MAX_IGN;} ignition4StartAngle = channel4IgnDegrees + CRANK_ANGLE_MAX - currentStatus.advance - dwellAngle; - if(ignition4StartAngle > CRANK_ANGLE_MAX) {ignition4StartAngle -= CRANK_ANGLE_MAX;} + if(ignition4StartAngle > CRANK_ANGLE_MAX_IGN) {ignition4StartAngle -= CRANK_ANGLE_MAX_IGN;} break; //Will hit the default case on 1 cylinder or >8 cylinders. Do nothing in these cases @@ -1115,13 +1159,14 @@ void loop() //Determine the current crank angle int crankAngle = getCrankAngle(timePerDegree); + if (crankAngle > CRANK_ANGLE_MAX_INJ ) { crankAngle -= 360; } if (fuelOn && currentStatus.PW > 0 && !BIT_CHECK(currentStatus.squirt, BIT_SQUIRT_BOOSTCUT)) { - if (injector1StartAngle <= crankAngle && fuelSchedule1.schedulesSet == 0) { injector1StartAngle += 360; } + if (injector1StartAngle <= crankAngle && fuelSchedule1.schedulesSet == 0) { injector1StartAngle += CRANK_ANGLE_MAX_INJ; } if (injector1StartAngle > crankAngle) { - if (configPage1.injLayout == 1) + if (configPage1.injLayout == INJ_SEMISEQUENTIAL) { setFuelSchedule1(openInjector1and4, ((unsigned long)(injector1StartAngle - crankAngle) * (unsigned long)timePerDegree), @@ -1153,10 +1198,10 @@ void loop() if(channel2InjEnabled) { tempCrankAngle = crankAngle - channel2InjDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_INJ; } tempStartAngle = injector2StartAngle - channel2InjDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } - if (tempStartAngle <= tempCrankAngle && fuelSchedule2.schedulesSet == 0) { tempStartAngle += 360; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } + if (tempStartAngle <= tempCrankAngle && fuelSchedule2.schedulesSet == 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } if ( tempStartAngle > tempCrankAngle ) { if (configPage1.injLayout == 1) @@ -1181,10 +1226,10 @@ void loop() if(channel3InjEnabled) { tempCrankAngle = crankAngle - channel3InjDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_INJ; } tempStartAngle = injector3StartAngle - channel3InjDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } - if (tempStartAngle <= tempCrankAngle && fuelSchedule3.schedulesSet == 0) { tempStartAngle += 360; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } + if (tempStartAngle <= tempCrankAngle && fuelSchedule3.schedulesSet == 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } if ( tempStartAngle > tempCrankAngle ) { setFuelSchedule3(openInjector3, @@ -1198,10 +1243,10 @@ void loop() if(channel4InjEnabled) { tempCrankAngle = crankAngle - channel4InjDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_INJ; } tempStartAngle = injector4StartAngle - channel4InjDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } - if (tempStartAngle <= tempCrankAngle && fuelSchedule4.schedulesSet == 0) { tempStartAngle += 360; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } + if (tempStartAngle <= tempCrankAngle && fuelSchedule4.schedulesSet == 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } if ( tempStartAngle > tempCrankAngle ) { setFuelSchedule4(openInjector4, @@ -1212,13 +1257,13 @@ void loop() } } - //if(channel5InjEnabled) + if(channel5InjEnabled) { tempCrankAngle = crankAngle - channel5InjDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_INJ; } tempStartAngle = injector5StartAngle - channel5InjDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } - if (tempStartAngle <= tempCrankAngle && fuelSchedule5.schedulesSet == 0) { tempStartAngle += 360; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } + if (tempStartAngle <= tempCrankAngle && fuelSchedule5.schedulesSet == 0) { tempStartAngle += CRANK_ANGLE_MAX_INJ; } if ( tempStartAngle > tempCrankAngle ) { setFuelSchedule5(openInjector5, @@ -1232,14 +1277,15 @@ void loop() //*********************************************************************************************** //| BEGIN IGNITION SCHEDULES //Likewise for the ignition - //Perform an initial check to see if the ignition is turned on (Ignition only turns on after a preset number of cranking revolutions and: - //Check for hard cut rev limit (If we're above the hardcut limit, we simply don't set a spark schedule) - //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; } //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. if ( configPage2.ignCranklock && BIT_CHECK(currentStatus.engine, BIT_ENGINE_CRANK)) { fixedCrankingOverride = currentStatus.dwell; } else { fixedCrankingOverride = 0; } - + + //Perform an initial check to see if the ignition is turned on (Ignition only turns on after a preset number of cranking revolutions and: + //Check for hard cut rev limit (If we're above the hardcut limit, we simply don't set a spark schedule) if(ignitionOn && !currentStatus.launchingHard && !BIT_CHECK(currentStatus.spark, BIT_SPARK_BOOSTCUT) && !BIT_CHECK(currentStatus.spark, BIT_SPARK_HRDLIM)) { @@ -1262,9 +1308,9 @@ void loop() } tempCrankAngle = crankAngle - channel2IgnDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_IGN; } tempStartAngle = ignition2StartAngle - channel2IgnDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_IGN; } //if ( (tempStartAngle > tempCrankAngle) && ign2LastRev != startRevolutions) //if ( ign2LastRev != startRevolutions ) { @@ -1283,9 +1329,9 @@ void loop() } tempCrankAngle = crankAngle - channel3IgnDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_IGN; } tempStartAngle = ignition3StartAngle - channel3IgnDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_IGN; } //if (tempStartAngle > tempCrankAngle) { long ignition3StartTime = 0; @@ -1303,9 +1349,9 @@ void loop() } tempCrankAngle = crankAngle - channel4IgnDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_IGN; } tempStartAngle = ignition4StartAngle - channel4IgnDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_IGN; } //if (tempStartAngle > tempCrankAngle) { @@ -1324,9 +1370,9 @@ void loop() } tempCrankAngle = crankAngle - channel5IgnDegrees; - if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX; } + if( tempCrankAngle < 0) { tempCrankAngle += CRANK_ANGLE_MAX_IGN; } tempStartAngle = ignition5StartAngle - channel5IgnDegrees; - if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX; } + if ( tempStartAngle < 0) { tempStartAngle += CRANK_ANGLE_MAX_IGN; } //if (tempStartAngle > tempCrankAngle) { diff --git a/timers.h b/timers.h index 75a4c15..8b319f4 100644 --- a/timers.h +++ b/timers.h @@ -24,7 +24,9 @@ volatile int loopSec; volatile unsigned long targetOverdwellTime; volatile unsigned long targetTachoPulseTime; - +#if defined (CORE_TEENSY) + IntervalTimer lowResTimer; +#endif void initialiseTimers(); #endif // TIMERS_H diff --git a/timers.ino b/timers.ino index e03475e..44d75aa 100644 --- a/timers.ino +++ b/timers.ino @@ -26,6 +26,9 @@ void initialiseTimers() /* Now configure the prescaler to CPU clock divided by 128 = 125Khz */ TCCR2B |= (1< Date: Fri, 14 Oct 2016 12:17:17 +0100 Subject: [PATCH 2/4] update processor ifdef to tidy code inc teensy options --- auxiliaries.ino | 6 +++--- globals.h | 24 ++++++++++++++++++++++++ idle.ino | 6 +++--- scheduler.h | 4 ++-- scheduler.ino | 40 ++++++++++++++++++++-------------------- speeduino.ino | 6 +++--- timers.h | 2 +- timers.ino | 10 +++++----- utils.ino | 4 ++-- 9 files changed, 63 insertions(+), 39 deletions(-) diff --git a/auxiliaries.ino b/auxiliaries.ino index 17e4673..d4a2874 100644 --- a/auxiliaries.ino +++ b/auxiliaries.ino @@ -21,7 +21,7 @@ void fanControl() else if (currentStatus.coolant <= (configPage4.fanSP - configPage4.fanHyster)) { digitalWrite(pinFan, fanLOW); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if defined(PROCESSOR_MEGA_ALL) void initialiseAuxPWM() { TCCR1B = 0x00; //Disbale Timer1 while we set it up @@ -64,7 +64,7 @@ void vvtControl() byte vvtDuty = get3DTableValue(&vvtTable, currentStatus.TPS, currentStatus.RPM); vvt_pwm_target_value = percentage(vvtDuty, vvt_pwm_max_count); } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if defined(PROCESSOR_MEGA_ALL) else { TIMSK1 &= ~(1 << OCIE1B); } // Disable timer channel #endif } @@ -105,7 +105,7 @@ ISR(TIMER1_COMPB_vect) } } -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) //YET TO BE IMPLEMENTED ON TEENSY void initialiseAuxPWM() { } void boostControl() { } diff --git a/globals.h b/globals.h index 5b88dc0..ded230e 100644 --- a/globals.h +++ b/globals.h @@ -2,6 +2,30 @@ #define GLOBALS_H #include +#if defined(__arm__) + #if defined(__MK20DX256__) && defined(CORE_TEENSY) + #define PROCESSOR_TEENSY_3_2 1 //compile for teensy 3.1/2 only + #elif defined(__MK64FX512__) && defined(CORE_TEENSY) + #define PROCESSOR_TEENSY_3_5 1 //compile for teensy 3.5 only + #endif + #if defined(__MK20DX256__) && defined(CORE_TEENSY) || defined(__MK64FX512__) && defined(CORE_TEENSY) + #define PROCESSOR_TEENSY_3_x 1 //compile for both teensy 3.1/2 and 3.5 + #elif defined (CORE_TEENSY) + #error "Unknown Teensy" + #elif defined (__arm__) + #error "Unknown ARM chip" + #else + #error "Unknown board" + #endif + +#elif defined(__AVR__) + #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) + #define PROCESSOR_MEGA_NO61 1 + #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) + #define PROCESSOR_MEGA_ALL 1 + #endif + #endif +#endif //const byte ms_version = 20; const byte signature = 20; diff --git a/idle.ino b/idle.ino index 3b8e431..37474a1 100644 --- a/idle.ino +++ b/idle.ino @@ -17,7 +17,7 @@ integerPID idlePID(¤tStatus.longRPM, &idle_pwm_target_value, &idle_cl_targ void initialiseIdle() { //By default, turn off the PWM interrupt (It gets turned on below if needed) -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if defined(PROCESSOR_MEGA_ALL) TIMSK4 &= ~(1 << OCIE4C); // Disable timer channel for idle #endif @@ -239,7 +239,7 @@ void homeStepper() } //The interrupt to turn off the idle pwm -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if defined(PROCESSOR_MEGA_ALL) //This function simply turns off the idle PWM and sets the pin low static inline void disableIdle() { @@ -293,7 +293,7 @@ ISR(TIMER4_COMPC_vect) } } -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) //This function simply turns off the idle PWM and sets the pin low static inline void disableIdle() { diff --git a/scheduler.h b/scheduler.h index 693d0c7..3f6fb3a 100644 --- a/scheduler.h +++ b/scheduler.h @@ -32,7 +32,7 @@ See page 136 of the processors datasheet: http://www.atmel.com/Images/doc2549.pd #include #endif -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) +#if defined(PROCESSOR_MEGA_ALL) //Refer to http://svn.savannah.nongnu.org/viewvc/trunk/avr-libc/include/avr/iomxx0_1.h?root=avr-libc&view=markup #define FUEL1_COUNTER TCNT3 #define FUEL2_COUNTER TCNT3 @@ -74,7 +74,7 @@ See page 136 of the processors datasheet: http://www.atmel.com/Images/doc2549.pd #define IGN3_TIMER_DISABLE() TIMSK5 &= ~(1 << OCIE5C) //Turn off this output compare unit #define IGN4_TIMER_DISABLE() TIMSK4 &= ~(1 << OCIE4A) //Turn off this output compare unit -#elif defined(CORE_TEENSY) +#elif defined(PROCESSOR_TEENSY_3_x) //http://shawnhymel.com/661/learning-the-teensy-lc-interrupt-service-routines/ #define FUEL1_COUNTER FTM0_CNT #define FUEL2_COUNTER FTM0_CNT diff --git a/scheduler.ino b/scheduler.ino index 28b77a8..4e4eb50 100644 --- a/scheduler.ino +++ b/scheduler.ino @@ -11,7 +11,7 @@ void initialiseSchedulers() { nullSchedule.Status = OFF; -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this // Much help in this from http://arduinomega.blogspot.com.au/2011/05/timer2-and-overflow-interrupt-lets-get.html //Fuel Schedules, which uses timer 3 TCCR3B = 0x00; //Disable Timer3 while we set it up @@ -35,7 +35,7 @@ void initialiseSchedulers() TIFR4 = 0x00; //Timer4 INT Flag Reg: Clear Timer Overflow Flag TCCR4A = 0x00; //Timer4 Control Reg A: Wave Gen Mode normal TCCR4B = (1 << CS12); //Timer4 Control Reg B: aka Divisor = 256 = 122.5HzTimer Prescaler set to 256. Refer to http://www.instructables.com/files/orig/F3T/TIKL/H3WSA4V7/F3TTIKLH3WSA4V7.jpg -#elif defined (CORE_TEENSY) && defined (__MK20DX256__) +#elif defined (PROCESSOR_TEENSY_3_x) //Configure ARM timers here #endif @@ -181,7 +181,7 @@ void setFuelSchedule5(void (*startCallback)(), unsigned long timeout, unsigned l /* * The following must be enclosed in the noIntterupts block to avoid contention caused if the relevant interrupts fires before the state is fully set */ -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) +#if defined(PROCESSOR_MEGA_ALL) noInterrupts(); fuelSchedule5.startCompare = TCNT3 + (timeout >> 4); //As above, but with bit shift instead of / 16 fuelSchedule5.endCompare = fuelSchedule5.startCompare + (duration >> 4); @@ -282,7 +282,7 @@ void setIgnitionSchedule5(void (*startCallback)(), unsigned long timeout, unsign //As the timer is ticking every 4uS (Time per Tick = (Prescale)*(1/Frequency)) if (timeout > 262140) { timeout = 262100; } // If the timeout is >4x (Each tick represents 4uS) the maximum allowed value of unsigned int (65535), the timer compare value will overflow when applied causing erratic behaviour such as erroneous sparking. This must be set slightly lower than the max of 262140 to avoid strangeness -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) +#if defined(PROCESSOR_MEGA_ALL) OCR5A = TCNT5 + (timeout >> 2); //As there is a tick every 4uS, there are timeout/4 ticks until the interrupt should be triggered ( >>2 divides by 4) ignitionSchedule5.Status = PENDING; //Turn this schedule on TIMSK5 |= (1 << OCIE5A); //Turn on the A compare unit (ie turn on the interrupt) @@ -294,9 +294,9 @@ void setIgnitionSchedule5(void (*startCallback)(), unsigned long timeout, unsign //This calls the relevant callback function (startCallback or endCallback) depending on the status of the schedule. //If the startCallback function is called, we put the scheduler into RUNNING state //Timer3A (fuel schedule 1) Compare Vector -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER3_COMPA_vect, ISR_NOBLOCK) //fuelSchedules 1 and 5 -#elif defined (CORE_TEENSY) +#elif defined(PROCESSOR_TEENSY_3_x) void timer3compareAinterrupt() //Most ARM chips can simply call a function #endif { @@ -316,9 +316,9 @@ void timer3compareAinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER3_COMPB_vect, ISR_NOBLOCK) //fuelSchedule2 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer3compareBinterrupt() //Most ARM chips can simply call a function #endif { @@ -337,9 +337,9 @@ void timer3compareBinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER3_COMPC_vect, ISR_NOBLOCK) //fuelSchedule3 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer3compareCinterrupt() //Most ARM chips can simply call a function #endif { @@ -358,9 +358,9 @@ void timer3compareCinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER4_COMPB_vect, ISR_NOBLOCK) //fuelSchedule4 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer4compareBinterrupt() //Most ARM chips can simply call a function #endif { @@ -379,9 +379,9 @@ void timer4compareBinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER5_COMPA_vect, ISR_NOBLOCK) //ignitionSchedule1 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer5compareAinterrupt() //Most ARM chips can simply call a function #endif { @@ -402,9 +402,9 @@ void timer5compareAinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER5_COMPB_vect, ISR_NOBLOCK) //ignitionSchedule2 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer5compareBinterrupt() //Most ARM chips can simply call a function #endif { @@ -425,9 +425,9 @@ void timer5compareBinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER5_COMPC_vect, ISR_NOBLOCK) //ignitionSchedule3 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer5compareCinterrupt() //Most ARM chips can simply call a function #endif { @@ -448,9 +448,9 @@ void timer5compareCinterrupt() //Most ARM chips can simply call a function } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this ISR(TIMER4_COMPA_vect, ISR_NOBLOCK) //ignitionSchedule4 -#elif defined (CORE_TEENSY) +#elif defined (PROCESSOR_TEENSY_3_x) void timer4compareAinterrupt() //Most ARM chips can simply call a function #endif { diff --git a/speeduino.ino b/speeduino.ino index 0f56e74..b182e4d 100644 --- a/speeduino.ino +++ b/speeduino.ino @@ -151,7 +151,7 @@ volatile bool fpPrimed = false; //Tracks whether or not the fuel pump priming ha void setup() { Serial.begin(115200); -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //ATmega2561 does not have Serial3 +#if defined(PROCESSOR_MEGA_NO61) //ATmega2561 does not have Serial3 if (configPage1.canEnable) { Serial3.begin(115200); } #endif @@ -460,7 +460,7 @@ void setup() currentLoopTime = micros(); -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this //This sets the ADC (Analog to Digitial Converter) to run at 1Mhz, greatly reducing analog read times (MAP/TPS) //1Mhz is the fastest speed permitted by the CPU without affecting accuracy //Please see chapter 11 of 'Practical Arduino' (http://books.google.com.au/books?id=HsTxON1L6D4C&printsec=frontcover#v=onepage&q&f=false) for more details @@ -775,7 +775,7 @@ void loop() command(); } } -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //ATmega2561 does not have Serial3 +#if defined(PROCESSOR_MEGA_NO61) //ATmega2561 does not have Serial3 //if Can interface is enabled then check for serial3 requests. if (configPage1.canEnable) { diff --git a/timers.h b/timers.h index 8b319f4..33a2f12 100644 --- a/timers.h +++ b/timers.h @@ -24,7 +24,7 @@ volatile int loopSec; volatile unsigned long targetOverdwellTime; volatile unsigned long targetTachoPulseTime; -#if defined (CORE_TEENSY) +#if defined (PROCESSOR_TEENSY_3_x) IntervalTimer lowResTimer; #endif void initialiseTimers(); diff --git a/timers.ino b/timers.ino index 44d75aa..1f44741 100644 --- a/timers.ino +++ b/timers.ino @@ -16,7 +16,7 @@ Timers are typically low resolution (Compared to Schedulers), with maximum frequ void initialiseTimers() { -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //AVR chips use the ISR for this +#if defined(PROCESSOR_MEGA_ALL) //AVR chips use the ISR for this //Configure Timer2 for our low-freq interrupt code. TCCR2B = 0x00; //Disbale Timer2 while we set it up TCNT2 = 131; //Preload timer2 with 131 cycles, leaving 125 till overflow. As the timer runs at 125Khz, this causes overflow to occur at 1Khz = 1ms @@ -26,7 +26,7 @@ void initialiseTimers() /* Now configure the prescaler to CPU clock divided by 128 = 125Khz */ TCCR2B |= (1< Date: Mon, 21 Nov 2016 00:46:19 +0000 Subject: [PATCH 3/4] create new master to follow josh 21112016 --- auxiliaries.h | 1 + auxiliaries.ino | 23 ++- comms.h | 3 +- comms.ino | 128 ++++++++++-- corrections.h | 2 + decoders.h | 23 ++- decoders.ino | 109 +++++++--- display.ino | 4 +- errors.ino | 6 +- globals.h | 53 ++--- idle.ino | 6 +- math.h | 94 +-------- reference/Speeduino base tune.msq | 130 +++++++++++- reference/hardware/Pro/speeduino pro NA6.fzz | Bin 285561 -> 285980 bytes reference/hardware/v0.4/schematic v0.4.2.fzz | Bin 214613 -> 215226 bytes reference/hardware/v0.4/v0.4.2_bom.xlsx | Bin 46170 -> 46797 bytes reference/speeduino.ini | 195 +++++++++++++++--- scheduler.h | 4 +- scheduler.ino | 79 ++++++-- sensors.h | 43 ++++ sensors.ino | 113 +++++++++-- speeduino.ino | 203 +++++++++++++------ storage.h | 74 ++++--- storage.ino | 126 ++++++++++++ table.h | 3 + table.ino | 195 ++++++++++++++---- timers.h | 2 +- timers.ino | 11 +- utils.ino | 4 +- 29 files changed, 1220 insertions(+), 414 deletions(-) diff --git a/auxiliaries.h b/auxiliaries.h index 42cdf2a..6168e6c 100644 --- a/auxiliaries.h +++ b/auxiliaries.h @@ -4,6 +4,7 @@ void initialiseAuxPWM(); void boostControl(); void vvtControl(); +void initialiseFan(); volatile byte *boost_pin_port; volatile byte boost_pin_mask; diff --git a/auxiliaries.ino b/auxiliaries.ino index d4a2874..2d3acbe 100644 --- a/auxiliaries.ino +++ b/auxiliaries.ino @@ -10,18 +10,25 @@ Fan control */ void initialiseFan() { -if(configPage4.fanInv == 1) {fanHIGH = LOW, fanLOW = HIGH; } -else {fanHIGH = HIGH, fanLOW = LOW;} -digitalWrite(pinFan, fanLOW); //Initiallise program with the fan in the off state + if(configPage4.fanInv) { fanHIGH = LOW, fanLOW = HIGH; } + else { fanHIGH = HIGH, fanLOW = LOW; } + digitalWrite(pinFan, fanLOW); //Initiallise program with the fan in the off state + currentStatus.fanOn = false; } void fanControl() { - if (currentStatus.coolant >= (configPage4.fanSP - CALIBRATION_TEMPERATURE_OFFSET)) { digitalWrite(pinFan,fanHIGH); } - else if (currentStatus.coolant <= (configPage4.fanSP - configPage4.fanHyster)) { digitalWrite(pinFan, fanLOW); } + if(configPage4.fanEnable) + { + int onTemp = (int)configPage4.fanSP - CALIBRATION_TEMPERATURE_OFFSET; + int offTemp = onTemp - configPage4.fanHyster; + + if (!currentStatus.fanOn && currentStatus.coolant >= onTemp) { digitalWrite(pinFan,fanHIGH); currentStatus.fanOn = true; } + if (currentStatus.fanOn && currentStatus.coolant <= offTemp) { digitalWrite(pinFan, fanLOW); currentStatus.fanOn = false; } + } } -#if defined(PROCESSOR_MEGA_ALL) +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) void initialiseAuxPWM() { TCCR1B = 0x00; //Disbale Timer1 while we set it up @@ -64,7 +71,7 @@ void vvtControl() byte vvtDuty = get3DTableValue(&vvtTable, currentStatus.TPS, currentStatus.RPM); vvt_pwm_target_value = percentage(vvtDuty, vvt_pwm_max_count); } -#if defined(PROCESSOR_MEGA_ALL) +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) else { TIMSK1 &= ~(1 << OCIE1B); } // Disable timer channel #endif } @@ -105,7 +112,7 @@ ISR(TIMER1_COMPB_vect) } } -#elif defined (PROCESSOR_TEENSY_3_x) +#elif defined (CORE_TEENSY) //YET TO BE IMPLEMENTED ON TEENSY void initialiseAuxPWM() { } void boostControl() { } diff --git a/comms.h b/comms.h index 30369e1..fd7822c 100644 --- a/comms.h +++ b/comms.h @@ -9,6 +9,7 @@ #define afrSetPage 6//Config Page 3 #define iacPage 7//Config Page 4 #define boostvvtPage 8 +#define seqFuelPage 9 byte currentPage = 1;//Not the same as the speeduino config page numbers boolean isMap = true; @@ -36,4 +37,4 @@ void receiveCalibration(byte tableID); void sendToothLog(bool useChar); void testComm(); -#endif // COMMS_H +#endif // COMMS_H diff --git a/comms.ino b/comms.ino index 26a6c8d..fd9ca7e 100644 --- a/comms.ino +++ b/comms.ino @@ -62,12 +62,12 @@ void command() break; case 'S': // send code version - Serial.print("Speeduino 2016.10-dev"); + Serial.print("Speeduino 2016.11-dev"); currentStatus.secl = 0; //This is required in TS3 due to its stricter timings break; case 'Q': // send code version - Serial.print("speeduino 201610-dev"); + Serial.print("speeduino 201611-dev"); break; case 'V': // send VE table and constants in binary @@ -236,7 +236,7 @@ void sendValues(int packetlength, byte portNum) response[17] = currentStatus.corrections; //Total GammaE (%) response[18] = currentStatus.VE; //Current VE 1 (%) response[19] = currentStatus.afrTarget; - response[20] = (byte)(currentStatus.PW / 100); //Pulsewidth 1 multiplied by 10 in ms. Have to convert from uS to mS. + response[20] = (byte)(currentStatus.PW1 / 100); //Pulsewidth 1 multiplied by 10 in ms. Have to convert from uS to mS. response[21] = currentStatus.tpsDOT; //TPS DOT response[22] = currentStatus.advance; response[23] = currentStatus.TPS; // TPS (0% to 100%) @@ -286,13 +286,13 @@ void receiveValue(int valueOffset, byte newValue) if (valueOffset < 272) { //X Axis - fuelTable.axisX[(valueOffset - 256)] = ((int)(newValue) * 100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct + fuelTable.axisX[(valueOffset - 256)] = ((int)(newValue) * TABLE_RPM_MULTIPLIER); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) } else { //Y Axis valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order (Due to us using (0,0) in the top left rather than bottom right - fuelTable.axisY[valueOffset] = (int)(newValue); + fuelTable.axisY[valueOffset] = (int)(newValue) * TABLE_LOAD_MULTIPLIER; } return; } @@ -303,7 +303,7 @@ void receiveValue(int valueOffset, byte newValue) //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; } break; @@ -319,13 +319,13 @@ void receiveValue(int valueOffset, byte newValue) if (valueOffset < 272) { //X Axis - ignitionTable.axisX[(valueOffset - 256)] = (int)(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct + ignitionTable.axisX[(valueOffset - 256)] = (int)(newValue) * TABLE_RPM_MULTIPLIER; //The RPM values sent by megasquirt are divided by 100, need to multiple it back by 100 to make it correct } else { //Y Axis valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order - ignitionTable.axisY[valueOffset] = (int)(newValue); + ignitionTable.axisY[valueOffset] = (int)(newValue) * TABLE_LOAD_MULTIPLIER; } return; } @@ -335,7 +335,7 @@ void receiveValue(int valueOffset, byte newValue) //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; } break; @@ -351,13 +351,13 @@ void receiveValue(int valueOffset, byte newValue) if (valueOffset < 272) { //X Axis - afrTable.axisX[(valueOffset - 256)] = int(newValue) * int(100); //The RPM values sent by megasquirt are divided by 100, need to multiply it back by 100 to make it correct + afrTable.axisX[(valueOffset - 256)] = int(newValue) * TABLE_RPM_MULTIPLIER; //The RPM values sent by megasquirt are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) } else { //Y Axis valueOffset = 15 - (valueOffset - 272); //Need to do a translation to flip the order - afrTable.axisY[valueOffset] = int(newValue); + afrTable.axisY[valueOffset] = int(newValue) * TABLE_LOAD_MULTIPLIER; } return; @@ -368,7 +368,7 @@ void receiveValue(int valueOffset, byte newValue) //For some reason, TunerStudio is sending offsets greater than the maximum page size. I'm not sure if it's their bug or mine, but the fix is to only update the config page if the offset is less than the maximum size if (valueOffset < page_size) { - *((byte *)pnt_configPage + (byte)valueOffset) = newValue; //Need to subtract 80 because the map and bins (Which make up 80 bytes) aren't part of the config pages + *((byte *)pnt_configPage + (byte)valueOffset) = newValue; } break; @@ -388,12 +388,12 @@ void receiveValue(int valueOffset, byte newValue) } else if (valueOffset < 72) //New value is on the X (RPM) axis of the boost table { - boostTable.axisX[(valueOffset - 64)] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct + boostTable.axisX[(valueOffset - 64)] = int(newValue) * TABLE_RPM_MULTIPLIER; //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) return; } else if (valueOffset < 80) //New value is on the Y (TPS) axis of the boost table { - boostTable.axisY[(7 - (valueOffset - 72))] = int(newValue); + boostTable.axisY[(7 - (valueOffset - 72))] = int(newValue) * TABLE_LOAD_MULTIPLIER; return; } else if (valueOffset < 144) //New value is part of the vvt map @@ -405,15 +405,34 @@ void receiveValue(int valueOffset, byte newValue) else if (valueOffset < 152) //New value is on the X (RPM) axis of the vvt table { valueOffset = valueOffset - 144; - vvtTable.axisX[valueOffset] = int(newValue) * int(100); //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct + vvtTable.axisX[valueOffset] = int(newValue) * TABLE_RPM_MULTIPLIER; //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) return; } else //New value is on the Y (Load) axis of the vvt table { valueOffset = valueOffset - 152; - vvtTable.axisY[(7 - valueOffset)] = int(newValue); + vvtTable.axisY[(7 - valueOffset)] = int(newValue) * TABLE_LOAD_MULTIPLIER; return; } + case seqFuelPage: + if (valueOffset < 36) { trim1Table.values[5 - valueOffset / 6][valueOffset % 6] = newValue; } //Trim1 values + else if (valueOffset < 42) { trim1Table.axisX[(valueOffset - 36)] = int(newValue) * TABLE_RPM_MULTIPLIER; } //New value is on the X (RPM) axis of the trim1 table. The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) + else if (valueOffset < 48) { trim1Table.axisY[(5 - (valueOffset - 42))] = int(newValue) * TABLE_LOAD_MULTIPLIER; } //New value is on the Y (TPS) axis of the boost table + //Trim table 2 + else if (valueOffset < 84) { valueOffset = valueOffset - 48; trim2Table.values[5 - valueOffset / 6][valueOffset % 6] = newValue; } //New value is part of the trim2 map + else if (valueOffset < 90) { valueOffset = valueOffset - 84; trim2Table.axisX[valueOffset] = int(newValue) * TABLE_RPM_MULTIPLIER; } //New value is on the X (RPM) axis of the table. //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) + else if (valueOffset < 96) { valueOffset = valueOffset - 90; trim2Table.axisY[(5 - valueOffset)] = int(newValue) * TABLE_LOAD_MULTIPLIER; } //New value is on the Y (Load) axis of the table + //Trim table 3 + else if (valueOffset < 132) { valueOffset = valueOffset - 96; trim3Table.values[5 - valueOffset / 6][valueOffset % 6] = newValue; } //New value is part of the trim2 map + else if (valueOffset < 138) { valueOffset = valueOffset - 132; trim3Table.axisX[valueOffset] = int(newValue) * TABLE_RPM_MULTIPLIER; } //New value is on the X (RPM) axis of the table. //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) + else if (valueOffset < 144) { valueOffset = valueOffset - 138; trim3Table.axisY[(5 - valueOffset)] = int(newValue) * TABLE_LOAD_MULTIPLIER; } //New value is on the Y (Load) axis of the table + //Trim table 4 + else if (valueOffset < 180) { valueOffset = valueOffset - 144; trim4Table.values[5 - valueOffset / 6][valueOffset % 6] = newValue; } //New value is part of the trim2 map + else if (valueOffset < 186) { valueOffset = valueOffset - 180; trim4Table.axisX[valueOffset] = int(newValue) * TABLE_RPM_MULTIPLIER; } //New value is on the X (RPM) axis of the table. //The RPM values sent by TunerStudio are divided by 100, need to multiply it back by 100 to make it correct (TABLE_RPM_MULTIPLIER) + else if (valueOffset < 192) { valueOffset = valueOffset - 186; trim4Table.axisY[(5 - valueOffset)] = int(newValue) * TABLE_LOAD_MULTIPLIER; } //New value is on the Y (Load) axis of the table + + break; + default: break; } @@ -648,17 +667,80 @@ void sendPage(bool useChar) //Boost table for (int x = 0; x < 64; x++) { response[x] = boostTable.values[7 - x / 8][x % 8]; } - for (int x = 64; x < 72; x++) { response[x] = byte(boostTable.axisX[(x - 64)] / 100); } + for (int x = 64; x < 72; x++) { response[x] = byte(boostTable.axisX[(x - 64)] / TABLE_RPM_MULTIPLIER); } for (int y = 72; y < 80; y++) { response[y] = byte(boostTable.axisY[7 - (y - 72)]); } //VVT table for (int x = 0; x < 64; x++) { response[x + 80] = vvtTable.values[7 - x / 8][x % 8]; } - for (int x = 64; x < 72; x++) { response[x + 80] = byte(vvtTable.axisX[(x - 64)] / 100); } + for (int x = 64; x < 72; x++) { response[x + 80] = byte(vvtTable.axisX[(x - 64)] / TABLE_RPM_MULTIPLIER); } for (int y = 72; y < 80; y++) { response[y + 80] = byte(vvtTable.axisY[7 - (y - 72)]); } Serial.write((byte *)&response, sizeof(response)); return; } break; } + case seqFuelPage: + { + if(useChar) + { + currentTable = trim1Table; + for (int y = 0; y < currentTable.ySize; y++) + { + byte axisY = byte(currentTable.axisY[y]); + if (axisY < 100) + { + Serial.write(" "); + if (axisY < 10) + { + Serial.write(" "); + } + } + Serial.print(axisY);// Vertical Bins + Serial.write(" "); + for (int x = 0; x < currentTable.xSize; x++) + { + byte value = currentTable.values[y][x]; + if (value < 100) + { + Serial.write(" "); + if (value < 10) + { + Serial.write(" "); + } + } + Serial.print(value); + Serial.write(" "); + } + Serial.println(""); + } + return; + //Do.... Something? + } + else + { + //Need to perform a translation of the values[MAP/TPS][RPM] into the MS expected format + byte response[192]; //Bit hacky, but the size is: (6x6 + 6 + 6) * 4 = 192 + + //trim1 table + for (int x = 0; x < 36; x++) { response[x] = trim1Table.values[5 - x / 6][x % 6]; } + for (int x = 36; x < 42; x++) { response[x] = byte(trim1Table.axisX[(x - 36)] / 100); } + for (int y = 42; y < 48; y++) { response[y] = byte(trim1Table.axisY[5 - (y - 42)]); } + //trim2 table + for (int x = 0; x < 36; x++) { response[x + 48] = trim2Table.values[5 - x / 6][x % 6]; } + for (int x = 36; x < 42; x++) { response[x + 48] = byte(trim2Table.axisX[(x - 36)] / 100); } + for (int y = 42; y < 48; y++) { response[y + 48] = byte(trim2Table.axisY[5 - (y - 42)]); } + //trim3 table + for (int x = 0; x < 36; x++) { response[x + 96] = trim3Table.values[5 - x / 6][x % 6]; } + for (int x = 36; x < 42; x++) { response[x + 96] = byte(trim3Table.axisX[(x - 36)] / 100); } + for (int y = 42; y < 48; y++) { response[y + 96] = byte(trim3Table.axisY[5 - (y - 42)]); } + //trim4 table + for (int x = 0; x < 36; x++) { response[x + 144] = trim4Table.values[5 - x / 6][x % 6]; } + for (int x = 36; x < 42; x++) { response[x + 144] = byte(trim4Table.axisX[(x - 36)] / 100); } + for (int y = 42; y < 48; y++) { response[y + 144] = byte(trim4Table.axisY[5 - (y - 42)]); } + Serial.write((byte *)&response, sizeof(response)); + return; + } + break; + } default: { Serial.println(F("\nPage has not been implemented yet. Change to another page.")); @@ -739,9 +821,12 @@ void sendPage(bool useChar) //MS format has origin (0,0) in the bottom left corner, we use the top left for efficiency reasons byte response[map_page_size]; - for (int x = 0; x < 256; x++) { response[x] = currentTable.values[15 - x / 16][x % 16]; } //This is slightly non-intuitive, but essentially just flips the table vertically (IE top line becomes the bottom line etc). Columns are unchanged - for (int x = 256; x < 272; x++) { response[x] = byte(currentTable.axisX[(x - 256)] / 100); } //RPM Bins for VE table (Need to be dvidied by 100) + for (int x = 0; x < 256; x++) { response[x] = currentTable.values[15 - x / 16][x % 16]; } //This is slightly non-intuitive, but essentially just flips the table vertically (IE top line becomes the bottom line etc). Columns are unchanged. Every 16 loops, manually call loop() to avoid potential misses + //loop(); + for (int x = 256; x < 272; x++) { response[x] = byte(currentTable.axisX[(x - 256)] / TABLE_RPM_MULTIPLIER); } //RPM Bins for VE table (Need to be dvidied by 100) + //loop(); for (int y = 272; y < 288; y++) { response[y] = byte(currentTable.axisY[15 - (y - 272)]); } //MAP or TPS bins for VE table + //loop(); Serial.write((byte *)&response, sizeof(response)); } } @@ -764,7 +849,9 @@ void sendPage(bool useChar) for (byte x = 0; x < page_size; x++) { response[x] = *((byte *)pnt_configPage + x); //Each byte is simply the location in memory of the configPage + the offset + the variable number (x) + //if ( (x & 31) == 1) { loop(); } //Every 32 loops, do a manual call to loop() to ensure that there is no misses } + Serial.write((byte *)&response, sizeof(response)); // } } @@ -902,6 +989,7 @@ void sendToothLog(bool useChar) } BIT_CLEAR(currentStatus.squirt, BIT_SQUIRT_TOOTHLOG1READY); } + toothLogRead = true; } void testComm() diff --git a/corrections.h b/corrections.h index 5344257..134a731 100644 --- a/corrections.h +++ b/corrections.h @@ -5,6 +5,8 @@ All functions in the gamma file return #ifndef CORRECTIONS_H #define CORRECTIONS_H +void initialiseCorrections(); + byte correctionsTotal(); byte correctionWUE(); //Warmup enrichment byte correctionASE(); //After Start Enrichment diff --git a/decoders.h b/decoders.h index c2185a5..1079892 100644 --- a/decoders.h +++ b/decoders.h @@ -1,5 +1,24 @@ +#ifndef DECODERS_H +#define DECODERS_H + #include +static inline void addToothLogEntry(unsigned long toothTime); +static inline int stdGetRPM(); +static inline void setFilter(unsigned long curGap); +static inline int crankingGetRPM(byte totalTeeth); +void triggerSetup_missingTooth(); +void triggerPri_missingTooth(); +void triggerSec_missingTooth(); +int getRPM_missingTooth(); +int getCrankAngle_missingTooth(int timePerDegree); +void triggerSetup_DualWheel(); +void triggerPri_DualWheel(); +void triggerSec_DualWheel(); +int getRPM_DualWheel(); +int getCrankAngle_DualWheel(int timePerDegree); + + volatile unsigned long curTime; volatile unsigned long curGap; volatile unsigned long curTime2; @@ -18,6 +37,7 @@ volatile unsigned long toothOneMinusOneTime = 0; //The 2nd to last time (micros( volatile bool revolutionOne = 0; // For sequential operation, this tracks whether the current revolution is 1 or 2 (not 1) volatile unsigned int toothHistory[TOOTH_LOG_BUFFER]; volatile unsigned int toothHistoryIndex = 0; +volatile bool toothLogRead = false; //Flag to indicate whether the current tooth log values have been read out yet volatile byte secondaryToothCount; //Used for identifying the current secondary (Usually cam) tooth for patterns with multiple secondary teeth volatile unsigned long secondaryLastToothTime = 0; //The time (micros()) that the last tooth was registered (Cam input) @@ -29,6 +49,7 @@ int triggerToothAngle; //The number of crank degrees that elapse per tooth unsigned long revolutionTime; //The time in uS that one revolution would take at current speed (The time tooth 1 was last seen, minus the time it was seen prior to that) bool secondDerivEnabled; //The use of the 2nd derivative calculation is limited to certain decoders. This is set to either true or false in each decoders setup routine bool decoderIsSequential; //Whether or not the decoder supports sequential operation +byte checkSyncToothCount; //How many teeth must've been seen on this revolution before we try to confirm sync (Useful for missing tooth type decoders) int toothAngles[24]; //An array for storing fixed tooth angles. Currently sized at 24 for the GM 24X decoder, but may grow later if there are other decoders that use this style @@ -36,4 +57,4 @@ int toothAngles[24]; //An array for storing fixed tooth angles. Currently sized #define LONG 0; #define SHORT 1; - +#endif diff --git a/decoders.ino b/decoders.ino index 2dd223a..d50f8a0 100644 --- a/decoders.ino +++ b/decoders.ino @@ -21,12 +21,19 @@ toothLastToothTime - The time (In uS) that the last primary tooth was 'seen' */ -static inline void addToothLogEntry(unsigned long time) +static inline void addToothLogEntry(unsigned long toothTime) { //High speed tooth logging history - toothHistory[toothHistoryIndex] = time; + toothHistory[toothHistoryIndex] = toothTime; if(toothHistoryIndex == (TOOTH_LOG_BUFFER-1)) - { toothHistoryIndex = 0; BIT_CLEAR(currentStatus.squirt, BIT_SQUIRT_TOOTHLOG1READY); } //The tooth log ready bit is cleared to ensure that we only get a set of concurrent values. + { + if (toothLogRead) + { + toothHistoryIndex = 0; + BIT_CLEAR(currentStatus.squirt, BIT_SQUIRT_TOOTHLOG1READY); + toothLogRead = false; //The tooth log ready bit is cleared to ensure that we only get a set of concurrent values. + } + } else { toothHistoryIndex++; } } @@ -64,7 +71,7 @@ This gives much more volatile reading, but is quite useful during cranking, part It can only be used on patterns where the teeth are evently spaced It takes an argument of the full (COMPLETE) number of teeth per revolution. For a missing tooth wheel, this is the number if the tooth had NOT been missing (Eg 36-1 = 36) */ -inline int crankingGetRPM(byte totalTeeth) +static inline int crankingGetRPM(byte totalTeeth) { noInterrupts(); revolutionTime = (toothLastToothTime - toothLastMinusOneToothTime) * totalTeeth; @@ -84,6 +91,7 @@ void triggerSetup_missingTooth() 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; decoderIsSequential = false; + 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) } @@ -98,26 +106,30 @@ void triggerPri_missingTooth() toothCurrentCount++; //Increment the tooth counter addToothLogEntry(curGap); - - //Begin the missing tooth detection - //If the time between the current tooth and the last is greater than 1.5x the time between the last tooth and the tooth before that, we make the assertion that we must be at the first tooth after the gap - if(configPage2.triggerMissingTeeth == 1) { targetGap = (3 * (toothLastToothTime - toothLastMinusOneToothTime)) >> 1; } //Multiply by 1.5 (Checks for a gap 1.5x greater than the last one) (Uses bitshift to multiply by 3 then divide by 2. Much faster than multiplying by 1.5) - else { targetGap = ((toothLastToothTime - toothLastMinusOneToothTime)) * 2; } //Multiply by 2 (Checks for a gap 2x greater than the last one) - - if ( curGap > targetGap || toothCurrentCount > triggerActualTeeth) - { - toothCurrentCount = 1; - revolutionOne = !revolutionOne; //Flip sequential revolution tracker - toothOneMinusOneTime = toothOneTime; - toothOneTime = curTime; - currentStatus.hasSync = true; - startRevolutions++; //Counter - triggerFilterTime = 0; //This is used to prevent a condition where serious intermitent signals (Eg someone furiously plugging the sensor wire in and out) can leave the filter in an unrecoverable state - } - else + + //if(toothCurrentCount > checkSyncToothCount || !currentStatus.hasSync) { - //Filter can only be recalc'd for the regular teeth, not the missing one. - setFilter(curGap); + //Begin the missing tooth detection + //If the time between the current tooth and the last is greater than 1.5x the time between the last tooth and the tooth before that, we make the assertion that we must be at the first tooth after the gap + if(configPage2.triggerMissingTeeth == 1) { targetGap = (3 * (toothLastToothTime - toothLastMinusOneToothTime)) >> 1; } //Multiply by 1.5 (Checks for a gap 1.5x greater than the last one) (Uses bitshift to multiply by 3 then divide by 2. Much faster than multiplying by 1.5) + else { targetGap = ((toothLastToothTime - toothLastMinusOneToothTime)) * 2; } //Multiply by 2 (Checks for a gap 2x greater than the last one) + + if ( curGap > targetGap || toothCurrentCount > triggerActualTeeth) + { + if(toothCurrentCount < (triggerActualTeeth) && currentStatus.hasSync) { currentStatus.hasSync = false; return; } //This occurs when we're at tooth #1, but haven't seen all the other teeth. This indicates a signal issue so we flag lost sync so this will attempt to resync on the next revolution. + toothCurrentCount = 1; + revolutionOne = !revolutionOne; //Flip sequential revolution tracker + toothOneMinusOneTime = toothOneTime; + toothOneTime = curTime; + currentStatus.hasSync = true; + startRevolutions++; //Counter + triggerFilterTime = 0; //This is used to prevent a condition where serious intermitent signals (Eg someone furiously plugging the sensor wire in and out) can leave the filter in an unrecoverable state + } + else + { + //Filter can only be recalc'd for the regular teeth, not the missing one. + setFilter(curGap); + } } toothLastMinusOneToothTime = toothLastToothTime; @@ -126,7 +138,8 @@ void triggerPri_missingTooth() void triggerSec_missingTooth() { - if(!currentStatus.hasSync) { revolutionOne = 0; } //Sequential revolution reset + //TODO: This should really have filtering enabled on the secondary input. + revolutionOne = 1; } int getRPM_missingTooth() @@ -139,10 +152,12 @@ int getCrankAngle_missingTooth(int timePerDegree) //This is the current angle ATDC the engine is at. This is the last known position based on what tooth was last 'seen'. It is only accurate to the resolution of the trigger wheel (Eg 36-1 is 10 degrees) unsigned long tempToothLastToothTime; int tempToothCurrentCount; + bool tempRevolutionOne; //Grab some variables that are used in the trigger code and assign them to temp variables. noInterrupts(); tempToothCurrentCount = toothCurrentCount; tempToothLastToothTime = toothLastToothTime; + tempRevolutionOne = revolutionOne; 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. @@ -152,7 +167,7 @@ int getCrankAngle_missingTooth(int timePerDegree) else { crankAngle += ldiv(elapsedTime, timePerDegree).quot; } //Sequential check (simply sets whether we're on the first or 2nd revoltuion of the cycle) - if (revolutionOne) { crankAngle += 360; } + if (tempRevolutionOne) { crankAngle += 360; } if (crankAngle >= 720) { crankAngle -= 720; } else if (crankAngle > CRANK_ANGLE_MAX) { crankAngle -= CRANK_ANGLE_MAX; } @@ -193,11 +208,11 @@ void triggerPri_DualWheel() if ( toothCurrentCount == 1 || toothCurrentCount > configPage2.triggerTeeth ) { - toothCurrentCount = 1; + toothCurrentCount = 1; + revolutionOne = !revolutionOne; //Flip sequential revolution tracker toothOneMinusOneTime = toothOneTime; toothOneTime = curTime; startRevolutions++; //Counter - //if ((startRevolutions & 63) == 1) { currentStatus.hasSync = false; } //Every 64 revolutions, force a resync with the cam } setFilter(curGap); //Recalc the new filter value @@ -221,6 +236,8 @@ void triggerSec_DualWheel() currentStatus.hasSync = true; } + + revolutionOne = 1; //Sequential revolution reset } int getRPM_DualWheel() @@ -235,10 +252,12 @@ int getCrankAngle_DualWheel(int timePerDegree) //This is the current angle ATDC the engine is at. This is the last known position based on what tooth was last 'seen'. It is only accurate to the resolution of the trigger wheel (Eg 36-1 is 10 degrees) unsigned long tempToothLastToothTime; int tempToothCurrentCount; + bool tempRevolutionOne; //Grab some variables that are used in the trigger code and assign them to temp variables. noInterrupts(); tempToothCurrentCount = toothCurrentCount; tempToothLastToothTime = toothLastToothTime; + tempRevolutionOne = revolutionOne; interrupts(); //Handle case where the secondary tooth was the last one seen @@ -249,7 +268,9 @@ int getCrankAngle_DualWheel(int timePerDegree) long elapsedTime = micros() - tempToothLastToothTime; if(elapsedTime < SHRT_MAX ) { crankAngle += div((int)elapsedTime, timePerDegree).quot; } //This option is much faster, but only available for smaller values of elapsedTime else { crankAngle += ldiv(elapsedTime, timePerDegree).quot; } - + + //Sequential check (simply sets whether we're on the first or 2nd revoltuion of the cycle) + if (tempRevolutionOne) { crankAngle += 360; } if (crankAngle >= 720) { crankAngle -= 720; } if (crankAngle > CRANK_ANGLE_MAX) { crankAngle -= CRANK_ANGLE_MAX; } @@ -453,6 +474,22 @@ void triggerSetup_4G63() toothAngles[1] = 105; //Rising edge of tooth #2 toothAngles[2] = 175; //Falling edge of tooth #2 toothAngles[3] = 285; //Rising edge of tooth #1 + + /* + * https://forums.libreems.org/attachment.php?aid=34 + toothAngles[0] = 715; //Falling edge of tooth #1 + toothAngles[1] = 49; //Falling edge of wide cam + toothAngles[2] = 105; //Rising edge of tooth #2 + toothAngles[3] = 175; //Falling edge of tooth #2 + toothAngles[4] = 229; //Rising edge of narrow cam tooth (??) + toothAngles[5] = 285; //Rising edge of tooth #3 + toothAngles[6] = 319; //Falling edge of narrow cam tooth + toothAngles[7] = 355; //falling edge of tooth #3 + toothAngles[8] = 465; //Rising edge of tooth #4 + toothAngles[9] = 535; //Falling edge of tooth #4 + toothAngles[10] = 535; //Rising edge of wide cam tooth + toothAngles[11] = 645; //Rising edge of tooth #1 + */ triggerFilterTime = 1500; //10000 rpm, assuming we're triggering on both edges off the crank tooth. triggerSecFilterTime = (int)(1000000 / (MAX_RPM / 60 * 2)) / 2; //Same as above, but fixed at 2 teeth on the secondary input and divided by 2 (for cam speed) @@ -462,7 +499,7 @@ void triggerPri_4G63() { curTime = micros(); curGap = curTime - toothLastToothTime; - if ( curGap < triggerFilterTime ) { return; } //Debounce check. Pulses should never be less than triggerFilterTime + if ( curGap < triggerFilterTime ) { return; } //Filter check. Pulses should never be less than triggerFilterTime addToothLogEntry(curGap); triggerFilterTime = curGap >> 2; //This only applies during non-sync conditions. If there is sync then triggerFilterTime gets changed again below with a better value. @@ -492,9 +529,6 @@ void triggerPri_4G63() if(toothCurrentCount == 1 || toothCurrentCount == 3) { triggerToothAngle = 70; triggerFilterTime = curGap; } //Trigger filter is set to whatever time it took to do 70 degrees (Next trigger is 110 degrees away) else { triggerToothAngle = 110; triggerFilterTime = (curGap * 3) >> 3; } //Trigger filter is set to (110*3)/8=41.25=41 degrees (Next trigger is 70 degrees away). - //curGap = curGap >> 1; - - } void triggerSec_4G63() { @@ -506,12 +540,17 @@ void triggerSec_4G63() if(BIT_CHECK(currentStatus.engine, BIT_ENGINE_CRANK) || !currentStatus.hasSync) { triggerFilterTime = 1500; //If this is removed, can have trouble getting sync again after the engine is turned off (but ECU not reset). + //Check the status of the crank trigger bool crank = digitalRead(pinTrigger); if(crank == HIGH) { - //triggerFilterTime = 1; //Effectively turns off the trigger filter for now toothCurrentCount = 4; //If the crank trigger is currently HIGH, it means we're on tooth #1 + /* High-res mode + toothCurrentCount = 7; //If the crank trigger is currently HIGH, it means we're on the falling edge of the narrow crank tooth + toothLastMinusOneToothTime = toothLastToothTime; + toothLastToothTime = curTime; + */ } } //else { triggerFilterTime = 1500; } //reset filter time (ugly) @@ -530,6 +569,10 @@ int getRPM_4G63() int tempToothAngle; noInterrupts(); tempToothAngle = triggerToothAngle; + /* High-res mode + if(toothCurrentCount == 1) { tempToothAngle = 70; } + else { tempToothAngle = toothAngles[toothCurrentCount-1] - toothAngles[toothCurrentCount-2]; } + */ revolutionTime = (toothLastToothTime - toothLastMinusOneToothTime); //Note that trigger tooth angle changes between 70 and 110 depending on the last tooth that was seen interrupts(); revolutionTime = revolutionTime * 36; diff --git a/display.ino b/display.ino index 8cca192..25c0b2b 100644 --- a/display.ino +++ b/display.ino @@ -56,7 +56,7 @@ void updateDisplay() case 1: display.print("PW: "); display.setCursor(28,0); - display.print(currentStatus.PW); + display.print(currentStatus.PW1); break; case 2: display.print("Adv: "); @@ -101,7 +101,7 @@ void updateDisplay() case 1: display.print("PW: "); display.setCursor(28,11); - display.print(currentStatus.PW); + display.print(currentStatus.PW1); break; case 2: display.print("Adv: "); diff --git a/errors.ino b/errors.ino index b06a766..0108373 100644 --- a/errors.ino +++ b/errors.ino @@ -30,12 +30,12 @@ void clearError(byte errorID) else if(errorID == errorCodes[3]) { clearedError = 3; } else return; //Occurs when the error we're being asked to clear is not currently one of the active errors - errorCodes[clearedError] == ERR_NONE; + errorCodes[clearedError] = ERR_NONE; //Clear the required error and move any from above it 'down' in the error array for (byte x=clearedError; x < (errorCount-1); x++) { - errorCodes[x] == errorCodes[x+1]; - errorCodes[x+1] == ERR_NONE; + errorCodes[x] = errorCodes[x+1]; + errorCodes[x+1] = ERR_NONE; } errorCount--; diff --git a/globals.h b/globals.h index ded230e..c50baa7 100644 --- a/globals.h +++ b/globals.h @@ -2,30 +2,6 @@ #define GLOBALS_H #include -#if defined(__arm__) - #if defined(__MK20DX256__) && defined(CORE_TEENSY) - #define PROCESSOR_TEENSY_3_2 1 //compile for teensy 3.1/2 only - #elif defined(__MK64FX512__) && defined(CORE_TEENSY) - #define PROCESSOR_TEENSY_3_5 1 //compile for teensy 3.5 only - #endif - #if defined(__MK20DX256__) && defined(CORE_TEENSY) || defined(__MK64FX512__) && defined(CORE_TEENSY) - #define PROCESSOR_TEENSY_3_x 1 //compile for both teensy 3.1/2 and 3.5 - #elif defined (CORE_TEENSY) - #error "Unknown Teensy" - #elif defined (__arm__) - #error "Unknown ARM chip" - #else - #error "Unknown board" - #endif - -#elif defined(__AVR__) - #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) - #define PROCESSOR_MEGA_NO61 1 - #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega2561__) - #define PROCESSOR_MEGA_ALL 1 - #endif - #endif -#endif //const byte ms_version = 20; const byte signature = 20; @@ -98,6 +74,7 @@ const byte packetSize = 35; //Table sizes #define CALIBRATION_TABLE_SIZE 512 #define CALIBRATION_TEMPERATURE_OFFSET 40 // All temperature measurements are stored offset by 40 degrees. This is so we can use an unsigned byte (0-255) to represent temperature ranges from -40 to 215 +#define OFFSET_FUELTRIM 127 //The fuel trim tables are offset by 128 to allow for -128 to +128 values #define SERIAL_BUFFER_THRESHOLD 32 // When the serial buffer is filled to greater than this threshold value, the serial processing operations will be performed more urgently in order to avoid it overflowing. Serial buffer is 64 bytes long, so the threshold is set at half this as a reasonable figure @@ -163,12 +140,16 @@ struct statuses { byte launchCorrection; //The amount of correction being applied if launch control is active byte afrTarget; byte idleDuty; + bool fanOn; //Whether or not the fan is turned on byte flex; //Ethanol reading (if enabled). 0 = No ethanol, 100 = pure ethanol. Eg E85 = 85. unsigned long TAEEndTime; //The target end time used whenever TAE is turned on volatile byte squirt; volatile byte spark; byte engine; - unsigned int PW; //In uS + unsigned int PW1; //In uS + unsigned int PW2; //In uS + unsigned int PW3; //In uS + unsigned int PW4; //In uS volatile byte runSecs; //Counter of seconds since cranking commenced (overflows at 255 obviously) volatile byte secl; //Continous volatile unsigned int loopsPerSecond; @@ -246,6 +227,7 @@ struct config1 { byte baroCorr : 1; byte injLayout : 2; byte canEnable : 1; //is can interface enabled + byte unused2_38h : 1; byte primePulse; byte dutyLim; @@ -285,8 +267,10 @@ struct config2 { byte IgInv : 1; byte oddfire : 1; byte TrigPattern : 4; + + byte TrigEdgeSec : 1; + byte unused4_6b : 7; - byte unused4_6; byte unused4_7; byte IdleAdvRPM; byte IdleAdvCLT; //The temperature below which the idle is advanced @@ -379,8 +363,9 @@ struct config3 { byte boostKI; byte boostKD; - byte lnchPullRes :2; - byte unused60 : 6; + byte lnchPullRes : 2; + byte fuelTrimEnabled : 1; + byte unused60 : 5; byte unused61; byte unused62; byte unused63; @@ -423,7 +408,7 @@ byte pinInjector1; //Output pin injector 1 byte pinInjector2; //Output pin injector 2 byte pinInjector3; //Output pin injector 3 is on byte pinInjector4; //Output pin injector 4 is on -byte pinInjector5; //Placeholder only - NOT USED +byte pinInjector5; //Output pin injector 5 NOT USED YET byte pinInjector6; //Placeholder only - NOT USED byte pinInjector7; //Placeholder only - NOT USED byte pinInjector8; //Placeholder only - NOT USED @@ -431,10 +416,10 @@ byte pinCoil1; //Pin for coil 1 byte pinCoil2; //Pin for coil 2 byte pinCoil3; //Pin for coil 3 byte pinCoil4; //Pin for coil 4 -byte pinCoil5; //Pin for coil 4 -byte pinCoil6; //Pin for coil 4 -byte pinCoil7; //Pin for coil 4 -byte pinCoil8; //Pin for coil 4 +byte pinCoil5; //Pin for coil 5 +byte pinCoil6; //Pin for coil 6 +byte pinCoil7; //Pin for coil 7 +byte pinCoil8; //Pin for coil 8 byte pinTrigger; //The CAS pin byte pinTrigger2; //The Cam Sensor pin byte pinTrigger3; //the 2nd cam sensor pin @@ -445,7 +430,7 @@ byte pinIAT; //IAT sensor pin byte pinCLT; //CLS sensor pin byte pinO2; //O2 Sensor pin byte pinO2_2; //second O2 pin -byte pinBat; //O2 Sensor pin +byte pinBat; //Battery voltage pin byte pinDisplayReset; // OLED reset pin byte pinTachOut; //Tacho output byte pinFuelPump; //Fuel pump on/off diff --git a/idle.ino b/idle.ino index 37474a1..3b8e431 100644 --- a/idle.ino +++ b/idle.ino @@ -17,7 +17,7 @@ integerPID idlePID(¤tStatus.longRPM, &idle_pwm_target_value, &idle_cl_targ void initialiseIdle() { //By default, turn off the PWM interrupt (It gets turned on below if needed) -#if defined(PROCESSOR_MEGA_ALL) +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) TIMSK4 &= ~(1 << OCIE4C); // Disable timer channel for idle #endif @@ -239,7 +239,7 @@ void homeStepper() } //The interrupt to turn off the idle pwm -#if defined(PROCESSOR_MEGA_ALL) +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) //This function simply turns off the idle PWM and sets the pin low static inline void disableIdle() { @@ -293,7 +293,7 @@ ISR(TIMER4_COMPC_vect) } } -#elif defined (PROCESSOR_TEENSY_3_x) +#elif defined (CORE_TEENSY) //This function simply turns off the idle PWM and sets the pin low static inline void disableIdle() { diff --git a/math.h b/math.h index e671cd9..bf94c51 100644 --- a/math.h +++ b/math.h @@ -1,96 +1,6 @@ #ifndef MATH_H #define MATH_H -//Replace the standard arduino map() function to use the div function instead -int fastMap(unsigned long x, int in_min, int in_max, int out_min, int out_max) -{ - return ldiv( ((x - in_min) * (out_max - out_min)) , (in_max - in_min) ).quot + out_min; - //return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} +int fastMap1023toX(unsigned long x, int out_max); -//This is a dedicated function that specifically handles the case of mapping 0-1023 values into a 0 to X range -//This is a common case because it means converting from a standard 10-bit analog input to a byte or 10-bit analog into 0-511 (Eg the temperature readings) -int fastMap1023toX(unsigned long x, int in_min, int in_max, int out_min, int out_max) -{ - return (x * out_max) >> 10; -} - -/* -The following are all fast versions of specific divisions -Ref: http://www.hackersdelight.org/divcMore.pdf -*/ - -//Unsigned divide by 10 -unsigned int divu10(unsigned int n) { - unsigned long q, r; - q = (n >> 1) + (n >> 2); - q = q + (q >> 4); - q = q + (q >> 8); - q = q + (q >> 16); - q = q >> 3; - r = n - q*10; - return q + ((r + 6) >> 4); -// return q + (r > 9); -} - -//Signed divide by 10 -int divs10(long n) { - long q, r; - n = n + (n>>31 & 9); - q = (n >> 1) + (n >> 2); - q = q + (q >> 4); - q = q + (q >> 8); - q = q + (q >> 16); - q = q >> 3; - r = n - q*10; - return q + ((r + 6) >> 4); -// return q + (r > 9); -} - -//Signed divide by 100 -int divs100(long n) { - return (n / 100); // Amazingly, gcc is producing a better /divide by 100 function than this - long q, r; - n = n + (n>>31 & 99); - q = (n >> 1) + (n >> 3) + (n >> 6) - (n >> 10) + - (n >> 12) + (n >> 13) - (n >> 16); - q = q + (q >> 20); - q = q >> 6; - r = n - q*100; - return q + ((r + 28) >> 7); -// return q + (r > 99); -} - -//Unsigned divide by 100 -unsigned long divu100(unsigned long n) { - //return (n / 100); // No difference with this on/off - unsigned long q, r; - q = (n >> 1) + (n >> 3) + (n >> 6) - (n >> 10) + - (n >> 12) + (n >> 13) - (n >> 16); - q = q + (q >> 20); - q = q >> 6; - r = n - q*100; - return q + ((r + 28) >> 7); -// return q + (r > 99); -} - -//Return x percent of y -//This is a relatively fast approximation of a percentage value. -unsigned long percentage(byte x, unsigned long y) -{ - return (y * x) / 100; //For some reason this is faster - //return divu100(y * x); -} - -/* - * Calculates integer power values. Same as pow() but with ints - */ -inline long powint(int factor, unsigned int exponent) -{ - long product = 1; - while (exponent--) - product *= factor; - return product; -} - -#endif // MATH_H +#endif diff --git a/reference/Speeduino base tune.msq b/reference/Speeduino base tune.msq index 0c6310e..80c1d38 100644 --- a/reference/Speeduino base tune.msq +++ b/reference/Speeduino base tune.msq @@ -1,7 +1,7 @@ - - + + "0" @@ -99,6 +99,11 @@ "Alternating" "No" "No" +"No" +"No" +"No" +"No" +"No" 1.0 355.0 355.0 @@ -115,11 +120,13 @@ "Off" "Speed Density" "Off" -"Bank" +"Paired" +"Disable" +"No" 4.0 85.0 4200.0 -0.0 +0.0 0.0 26.0 230.0 @@ -127,9 +134,9 @@ 260.0 3.0 14.7 -0.0 -0.0 -0.0 +0.0 +0.0 +0.0 0.0 0.0 0.0 @@ -204,6 +211,8 @@ "Going Low" "Yes" "Missing Tooth" +79.7632 +255.0 3200.0 -21.0 38.0 @@ -412,7 +421,12 @@ 0.0 0.0 "Float" -"ONE" +"No" +"ONE" +"ONE" +"ONE" +"ONE" +"ONE" 4500.0 3000.0 6000.0 @@ -570,10 +584,108 @@ 100.0 + + + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + + + 700.0 + 1500.0 + 3000.0 + 4100.0 + 5500.0 + 7000.0 + + + 35.0 + 50.0 + 65.0 + 80.0 + 100.0 + 120.0 + + + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + + + 700.0 + 1500.0 + 3000.0 + 4100.0 + 5500.0 + 7000.0 + + + 35.0 + 50.0 + 65.0 + 80.0 + 100.0 + 120.0 + + + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + + + 700.0 + 1500.0 + 3000.0 + 4100.0 + 5500.0 + 7000.0 + + + 35.0 + 50.0 + 65.0 + 80.0 + 100.0 + 120.0 + + + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 + + + 700.0 + 1500.0 + 3000.0 + 4100.0 + 5500.0 + 7000.0 + + + 35.0 + 50.0 + 65.0 + 80.0 + 100.0 + 120.0 + + - \ No newline at end of file + diff --git a/reference/hardware/Pro/speeduino pro NA6.fzz b/reference/hardware/Pro/speeduino pro NA6.fzz index 140c5665667cf87be208c1d09f4dc2e773df4118..6ded916f19596940aeab3aa80054ec1edba42a69 100644 GIT binary patch delta 237681 zcmZ6SV{~NUwyvX&*>O6y?WEJOZQHh!ifx-6R&1wZ+ji2it()HG?lZ={ziN$9Rjbyg z`ObGfv!0t!vf5A(-}KVVZLt&2uU;?@nThHfrE_p z=z*Ytq%x^NU?iX-fEg_&BB5deMx^7?i?rQI0_Sf|t9iEhiNUNOz%CHC^He6Hq-pVE z_HuJCEA-xDw+Jpc^kz41H|F-1s_g@GZDxl~OETMsV)eRTj)&};PZmC(ram7Q`sOB% zUY&0Z+?qe<-j0uU!IJ(4b;%)1)yO_I&c20w_n25lA4n^D>$`94QV`AqWCD@?zPx+M zqJ6JscUyYc-JjbVQu}<~Vn2L3d7oRjIOO`gx`~weSnuBOem@!7%IWHUA=!A3)4i<5 z|Fv0SxAk^#`K@5Vg?!4-Fx+G@5 z)juBqCtiW4dcQ7m_ zSof;-#+Hxw+jZHSHNo}y#qkcq@%xvbrIunmxDb%>Lv3p5DjzrGq8b{r%Xo8Q|;l-XJx>u%YA2RR1~l z(OjZ;aXyiohy?KUyk}0a=FN4L%bnf$KzeMuL7l+W+W@c=NZdEFXXnx--8e_dZ+&|< zJn10Ts`<3Q+*PZ-%|-h3ZCKXT-_b9!sjI%Zr-`JfJ~0EBsd-E1;?KTc#7^Ws$T-rb zr>}0Y#J}u`7mr!#67Uu8z*F1|fYd4J*<9pS0c4^D_udpSK&QhM%jd7_(zQ zav3WM%*oVR9bsKP-`ATtevh7nqv|lSB%`dzND)d=GCNPb6`3e#UYtMft87uVzO%(| zBNrXH7x>q1)stAB?=;R*s&o3>gI?YzJEdbtf8_y=XqZ8!lH%uZ_c6yVpLwHBIj&|C zthv|Hdo8AIj)HTeym)(4+6|^I%-J@7-x(6sw%RDf)wR&Lvu<(2cs$EJ?8ozIA5HxAl|n{pRuWd4r=HnSS~q^bYR5 zn{OTP{sAC}2bIBUhsXZweh6Y{W7pvE_IAa$k-WdO;c)wSx3@63SsM(yGzmA!Vm!%G zst;2_OMw$KBjH#3#!uI-FCu!9}88=5*x!2i!`r2ze-C=riHKTh9?NmxUUzn|q60Xg1fIU@mx9(P>1UXy++H8_7OWvk4*>p2p{F42|Syza1B)TKT5z-EM^dZLZ;K%t<_dalQkekZzs zlN-4Jj=x;0dHci$LVZ=RSvOy=u4$~0Xq&Rwn!va1A1xKhI-zfE*DCS-=ryHi@tOwE z{(B~iDN&+Ys*My(ru3Zs=d^vs-z{eJorVn<$`C>A9IHI+t3Tu*3zWX~1U5`$UGb2n;&ztZ}yc1r1YU24=p5JM|yDb~>Sk7d(}Emq1}am(D=MA7gfZr^gita)_C zh?Fg(wo)ZURJOG{p%BY4 zH#y>o!?@^~7KJI}*~|d0xEpIIv*?k~+Rg(qDlk(EwzUYu%sVohL`5cmgX{8<9~3(& zB;zq##9+YCN_+#~fTe~kUBM(fF`~PEpt{0l+B#2>`p#1&QiB3iPQChbBQ&L_@5M3~ zQiI`w_@xml5W9ql%^V*r*9PU0>zKvW6L;-5V%1I zj|u;SC^j>cp@$f=C?gpF==y54tRb|PQGEUKrX%On(*8o`-vaA{1XyO1EDN0=Hk4tj zt2**F1^hitmIfwdV{$_m*5d%;Zf3SxWDbZo1Fk^9`XoBBP!(-;QG9?ij*ha2UT_#8 zA4?kAPU%cD%90kjQm^~sq9)n>R z3Xu)1%yT++4fi16`jKpJsHB}^9~4ZTxl4SSJ)W^koYBzCInhSuqqcrD_LV+(t$pY9 zD(_4trY36Iws8f}M2%P0sfzV~T)qDKp_7{71@!?G69>C}7o2KlEr^tQc>XXO7H-(NOY`SF4 zRFX%$ho(?B-oO1ZTGW%)39+j<*~}y0_)AOkl(TNsX7{py*ETE>?hzKTlENmDcv(jq zlPS9d8nWo!tbNPO%+^WA#(J5odhK&7mN5;!6I#?$+QjE6<}#nqVbk>ah1K+l8-u_| zPP0}V3ic@tzOzTEOlmNt0*W^g{_`J-U*c)WEiVDKoubzFgc zZeVaPZq`B$=9ICcnSISk;i0P~qnCXNN1~MYB42rfcSJK#-zw8Vm6r>donf1HMX!?QJ-~C=vYxZc{BdFQs`Lv@514^ z^uwZ}p4Um6kJ_EP6?400-J*iQ}$s1u=~Id2n5%0>QSeA_0B2;*L=NkNFHC9p#Swy9j>cg3EKajuGzmZ zLH*waOG=ES;sEzpz3Fu$j&kiJ;HCl`Hzq>XBX_SWL8+=hxi2@T+HNis_`c!NxU2j; zMx;_KZusESTQ@7Oo?p#8!!nbRxhFl!jverWfRG4p7WUXYL z1B1}dg{Kg3U&{?-x`Tcz3$hNIS=%*%svbU?(!luoZdxZcA+Kass=jAj0xK8aBV3As z$=!!cbJ1-ie3+GxyfxL!>K)Y>Q=bcxKO;qi4}Z;>qGZ#|eTj~m6#b^!8lkU~SEJ(T zQzO@`@K9mSao4C1K1Mu%bRn%zUC*XbeVbR;M#!0B_eLAo>j(9)KoRx0FgYQ(pzZ`e z)E$<=I$285R*kcNXT8*+5irG;6-33)6|1(U4`-bAvi=-nsXO>$`Mgf4uD2oeE3!#K zFYIhWRmx^^Z{Y!bEy}Iyw$Zhzi{+lhlA*{PD+77d{`}I7Zy= zWXLJs%%9ZsCwmk4NtpNi=3M#QzTMu54O0?Hh*lXd?v?6P)ti`;%kX!7L)+ znzoHR;9&Mcp%bg+KpqdYx>N@&qL zr6iJx2gHXfG^X6qek)Uz@qAIEVHFt$Ug3x*TxT|x1JLWuXr+`OkJ*N2p^;v=wipwO zo*c?FF};iHoRzV8H`8-66<5)4{gcd`oEvdHI;veCGG7%WZrsg0AGEdYU{fHbQCf=7kG@2Jb%|0ecobp9yTmiU&=EL!_v`A@1EfaDoNml%06wNMIUie|B}NS7{# zBR0@PeU6Ytv_K!uLn3jP&shVCn6CjGQfmP!e}p3C{t@^UqpnAnu2WQ2tQHx*r3eZo z!@uj(Sg*DkgX8aB)9K*B*9Pi>EUba zYT}WhcaGcC)tX+AVdja*{PT8rukdRE4qL1DB)NWjZY5Xi0+_mi+V=~m?L$ZJ3wr<; z&1ao!n%B->TP1+@XQKD3d*G)}v(#tK=Wg}TGf~iwrw`}G?)R64?(WT^DY;y?rQ=(x zwW9r%DA&}kj|uH;@2A_j8Q>&O-CIc~hbw|qj4UUP{qyT%``yXmKLcC5%6E9s{E#B# zIXoR?WT6A!z1GB#v=+kP0flu-1IQZF`k1;T{S>x-dIm+Y-UM|3x?70dY!)vGFI5yj+eW?IJ@{Xb4;CRr?$#0U?ST`IVX; z!+a*AY3W5XR?&I)9Y&O0z+lv2D=L?taWV^Yf^n@MsZ@jzp)j6jLKCNXBB{U00I2s@ z38Lr?$z9XudCaM7kV_7go3geH(L)2RuFFRE_>l<`RW6?cJZB+1 zOz9UeBGkdvCYOloqE9dvGEBVmI$pG8Ai9E+WS|4q9|GEeRM9&O;IGtvfuvynQWDOE z7Omrlwoo%&twAPQ$1s%WgB^DURID_aJXr}kTTbVyb;$u)h@E+WbatOD#WS3<_e1QS zHrPSn@Pl_FsK2>af$AWgT@J;fA&vG#wrJ>XM6MlxQAi4Rjuh4ZRx_AmWO~tg5|=tq z@d9FWKOV74Z9d_#i<0XQTm8qCu zXl*salGSPS9mpc<4~D(@(d1h$jNtiomWP&MqBLG8{SBl+kfVF(txH!JulcC{Ss0+>V9j~$j6qJ1Vc*wO!fHi(K{%p0L zk4M{rXqdeCY*g&{9Yrmk6|926iJ(lv)gX;a zhZQU9QUyE!hODfRhAisVgd`uCPR=J7%N+3slOM`IwU zm)GjWX%-vGkyMcDaQd4Ez7F6O9UDVf^B`lVFUWm~J1U4w9lJp&QO<3R9`+gmP|G@d zC_D8(+Wg6!sq~ppDm~TKAJ#S&gm`-D+I=Z{s6!tCV^Cg&psq7VYpffostRvyrgYq0 z+vGl7o5`4O=hu82c3rJ&HonpX;}&YKQ|->TS$urI&R;I>b}slDef)&AVbXs zxm!3%S%r6&e^$>*q3KoYuIR=w9Zv; zntEh{xIV=nxAZ#YF4v|-w4lb0^AD8K z5ACzf%YWBre}`ACk@X`<2tx3Hv*U3A2xJDuLxiuN_M?YQz!>f&5H1<>(v@3E6gFK8 z)|0^06L5E>r56kxA<4CePJjc}TH^Q8r8vZ352M_l6iLs$eCM;7HWW9+1d(Ei<^I|xmMJ6T02>nAh|U|IIX z(EyAi3c5PDrNMKs;=3T+(y!qO7{_`+f~v93KEY(FOjQI>iVKN=`W(9Mxp3eR#g;*l zAh*L9&PrY9{a8vgdtJb1(`my=nLHs+Q~G4m3#CjY5pQ!ZlD^+u(<>G@W}JO9U+L9; zE1oKr*VuQFI0QoQEgNkxc(&RffT23-QfOnIXL!Wsbk~gSJZ>fbLZHi|WK0XQV!v z{Dw%;w3($^;&bCZh3rD=Ik$AP?lS}3ql+)tb4;uR@&1qGsM>^XoW8u>^Y*+GshQ8d z$f#$JNpEds3%nzXy6s{g;Jc?xGK6%pj2+On98JHRm-8MXfPSW}Ub?22HNy z^HPgxdBgR-bYPL=!)ymW-fx-<U^nfp=`c$$*)u7d_TS_q-@?=F{YrO#O6Kt+1skoA^Fu|bN=YTWn>tEEx-496 z-yv3o=Kuu#u&*IZgDf7xFsdHiDM9^4I6d^tVN1=e>%375f&# zU`DKZaxx8Y+j&~Btfg?;(D0o48ZweS)Cx*q6i<1$x`qtDf$dc=h>~*T`B*T2JvE#; z^*)z$3kbppD9q!Zv@g>z80B@tYqk-YDrmrE72#H}CFJV-lB5*n5NC0zaxDZ!iVYk1 zx+cR=##K{(KalRBE!zk zd~1)+q? z-2J8qhyc;4OJC3gck<-(4rba_QR#HFlSgE$AJdE5r)>usPuB(8Ek9JVDaJ3 zKqa{FrbtLjGfNRbG^_U1CNV67ge>?(0lqe?S`LVj3u0xR7yYrT=|Vzvz2g`(_ewwU zXs93P+bD^7#YVgcawq>8AsIDkdjZtWuL&KhiXNOSbq8!7=v@J&TA+|FMr--PO<}Z1 z<>Fb-?ktieNb?EbpgyEM4S{>D>{9YP)RR8fBIy%|cKKIMg#4$-+aKboTyo_HV4}Db zdZajF82_Oh=>MX+TX1-!D&P|7o>EkJ_oqB&h|u{q}j?TnCiw~#OD=H3^X0}7>qc&bben|lrf%+i>t_9nS4XU_mFjqo~ETs z;t*c@6b*ZZNWNW_=NQ2&|J(MMKqOGoH?WEb1&lZqyo_wEjCmUg3zrbeKVfl5ZCdOx zCK20aLFE$}{%R7tNSuCJE(AvU;4R4%!|FuM)OB#w=(MY=+W1Kbyns9{{`!JAGjpdE z=9J@jaPySpjv3ZSs3MIPaZVSN?ixip6uzWX6u#*+zM7NxA+D6m#0fd8MgNU~ zdD28WVnBoj(X*OUCl`-NQ5Um(g%^kl<`@SNT=$6*fF~?F6@rsXibu+i8f~(lXK-Z+ z$|KQY6@>3vNU5!Wr3bU8CZ|X49<`8{RcVlLIG$z?`AtIL39v#da@e5s*Z9xcnCru! z;-sng&?@;6z;MsaP1CFwHQ+XP_IV41Zf`67g9|8J4(jqu+ree1o$u%T#~Ap5^GI{_*O$nN_s8;m_(;*v-d zr1mBl^^{2UzuBDDF#29g_2a5hTX(aYIkEb`WZv$7lX;V)&Q7MK*I8N5Xo78h(QLWDwZJGf3g*<+h_g?0nPT0HTBXUi*v9< zbHl<_0o#z#3D2h2%P&+&V7JQd75+l}`O9<(Y5Am^cjK~}uNZI;qT{t-TR#aU41PZE zAxt393wdrr+-gEOBiN{ful}?3H$?PItbX$(-2?2gqBHa$OhKhfD8Hj+LVeRaH-k*T zH+qv0!xYATepAS?anFYn#9T8z5)z*r;hM;^_M;bGr$Xby+F?~ewH|l)Z=Qlx;?xVZ z1_?FAh`u>vE~O$6$lr`cRxd+hZMuce=!)^qDfLa!{1r9o@=} zoW+reqv@w;cN@g#`y>UWEh@x!)Z&19I!#+Fh7DI!K+ZXzi%DNy5rnv)eHE@3n2bma zjOgJ|3lwD0?*q^&$BW!~)Z0t8Ng%#1oGFo7yw;zX7-G5D zed7qY1mj05ifA!<4mYhz~({}0+1YKIBBo|KuBhi~O zrZ*em2vvU$iRXcs9zfBm3RG>PH@~~O-A_x_w

z1>$Zp5S&Fr3TN?d6(4s+ar9JM zm1;*)cdi1u`%AXrRsJ8ZLWW~BBC%!CY=KdyscWXpP+3H*Y5aMQ$o|h2seQV)d#(QP zZS{D)^ELxFf`XjQ_%blW3Y$k8C9Iv#-j7cW9_?Ny?N5ChK9&IL>W^b4lGguqNKAIe zQ(f(6vkV6anuYZu-!WlLCBu9W9H}baatNo3fFyPN`nM*O1b})v*a$W?mld3~P+Mt^ zRB$8m!H?t>W zd5f1Q`SHf0EF1b!hFJ{bpe*!FmEr^kA=d{ew3_6?+oDpZMT8K&>us@2AYhF@dHsh@ z)|nLX%doWsGXO#c!00YIePdg#AJkwwwkX7q$}d-8IKZnh=NY=0%m;R2RwBO7B8C8|ek3vMc{ zUJ+W0C4jV-m;A)x&m1=>Y)iRV99WelnH{fMr#SgYx^{BvF)5z$J=lbiUxn*G?PMI@ z&0C>eAY#bi<4&A+ya}QIL(sYxtB9~2i^-3)9s8v^XCSgi;2aoM5q&yA3z#H zL#ez+H*Mmmz{6^l&<=B37if$)Ds@ei!Fn11{w!)KY@uo|UbiKES4c#LXg$=!n7BTU1S$`LNYj z|2c#e1rl-qNAV~2E7x$(AQPOeDC9L9wy^qDKph5Q5I|>?PRNZ@u7f{EqXBeCxKdm>S=qt(^8Z;WVoI%jZr26=_xYz z426HXkQa)R^|nF!{_Q*VKQ(Q9!&gRXWMZZZDZ5iz8R7#>!KKN|5Ll~_Y%wM(@+iXz z<Rr2Wt%rjC% zqd!SQG4SKEt}p~_@F9hxX;TQJUZv+yv(l`e!x5lVw@dQ?x>D2g|F^o(di@A`beIh# zeG7nb{p3Eq0(Su99U`GoNQ=K}Sz3uM1Iik=8%9n}4fhGhz)i4OupS(p=vE$kd3EhXr=|YW0~r{yzah^! zjg!{1=Jw2Y8xOqZ3L(Y8FpNXMuT{5un~Y=Wg9GMxbptEX5wGaGXs-WJngh#jxruV7 zONNc2hv<(t=kD$o^~AZowSV#<^D@as@M#iM`~Ifg?qx-~O&nY$yHQ(9EBQv0As>SW zE(}A~*c`8ODG+MU1uosPO)~CM59#o+nM6kquJlho?EUj+6acR7!Ur)ebSk})c6`F4 zHO}ZtW#gu^g51b5^eJX*$0^47yrO6o&0>+#Ce6kcPXl5-XHV6n>HkKpz!X zSI+P@g1D(YhS0HEj8f~Nn2k`usaCN18}y>euCWOqXBqWq5Lm_2e?-^7J34N055WjU z|1fh!jmtD@)c_y~{S{|tVhI?e$b_yCB+r};aDt21Ra&J?bvr*bYd#~M4> z6PWO{d&g%Y-1eZNXrT%D1YvBDVJ6DCE+87&*Az}Lp5=16r;0+PJB&AG4%E01ju`m6 zOu_!#CxROB=Ayop&?H<5M_*^i2cIBQ5CkXAe*F!bzZx)#jV)L0%rAS)iuVm%(BUAEW-QusDB944Z}^&(7}!PDP0|?Gxo+3SPy^ z#6J19Re|ZN@}P_mthXmzIwwBiSK?xb7N-)+*%+}4y`g*$ zW^x1pqXBS5Ox8KhPan#{FuD@Y`xiFx$o7QU(VwEh9tG{icA;4PxW243A^36@MTnCLeJ*zheV1XlBQ>$%FQX{A>F( z=kcCKV$7bGBebBh+1S52hRgy-0mv$HP>csaglgO6OE#jogO{@JD^^4jCU2reBV~Ol ze-{T*8Dm@78c^MSg9STd!j~XFVyEFgwLorJWld2bQid+D9I$0oD8ek$_q3JNH)v4G zrs7M Mr!I`myFw_5XD(S}}=+W)M;M9OWp#8P4AffucOUtIu9ex`JSFM;V?!N%J zYl@&7CA+8wSD@@sCWbZYa)SDDda&U^Y30SeKV{!Z?EFvY%$kWz}o`YB(-2|r6=FpuhSWA&HoD6LvTUmjD}4w3&}x2($oaOT~%pgjg4^B1Aj z@0GX8;1y-@A6g63a=r+O?1m#zYHSsBYS;>1+7b$Fo(san)KDtWr zr{o8PwV&1!xHHHMwV5&ROaf6j#xKP*fU;L0w`5oW)cGJ+LvJ)BG~VUoLZJk8zmQ_= zKaYHqmVWYWa4&|~BP>XeZrl6osE%i}n9YG23Z&Z+m^UsyZD*jDbMa%1Rmdgw6~k9T zfSY2Iams8q9E0Q#7S3S`gC^3M*pVihKz0c-uY7l|r6E`@KsOXWTNiF34demTm4C&y zOl56Kf!~M+nbGmm%wI?KuJ$!8nFq+d{Y|M2$GD=#ItEAhuOlA?&|8-46xF_s(wM6S z6Zlo`sjtAXtSGRR)iVqu_i1#Rl!f2Hyx8g&YP6g6AWta{r@c8%4-k&S*GeWoA`gIf zU!7USKs(NJks%T18KDuEAP1l>UAsu-_bpQia#uDV$qQqm9<%cfku9l5Kcqt|{GL?G zp(K|Zb`YQ+_(o9FvR_6=|5%in4t#+KFxL8|3>raxTCmD7(*$j=!{u45nriER;?Fc4Cc;sodh0mHbxFv9ib z&#h9~38t`W#}OzvLih??l2G@CN-9bLq4Du7ugX;*?mAiy{xpAatF$+aDs>^sC_3ou?I-VYIs z%bFuK3x+g3D(;(wjs!$TAae?&Nn7X*QY*~6rca%Zt@;YeOO(CNvzXvmcWF>@t@$}s zOf3De7SFU((2TDm{!>dvL}u5KgKZDWn;80Cz$#i;uRXi>e2zu&9~(Lks==yM1D zwIO<~WH#w30Biw(J;X{lLS1>@P~lyWQP_G*XiJ%iwvd6_R}0BoX=*-$i>1@2PF|W# zh?3B73={@!BErJEhEw4`ZbQ;bvi(!fZ2W%#qfv$^p&_-_^|xxV)g%}Pz}c$}71j7}4q3uIDiA$pa1$6~ zjAUrxdkD(pmO&ntea5NbYrjqcOIsRfA8P)OLu6KH|KD}-CnPq~{Cl1;>EFP<5QMoQ zI*T#{wPv1(UjM<02;8RkFPWKu)K^Gq&FwQYO^P%rHJT86(AFdH8|T!8?>AB9-#q3J z$t3`S=`>n|q+yd51@&o3IXxzD&`u_B5GZ2qPzUVpA2*CPZYzn@Y?=rzZB8 z{E5yk8K10EnRfUFAwxRHSu@93UJmV;AzqcBtQ=1}T5OI3v4Gny_fsijsW6}c>JLCc z*j&%%7!=fA6B5DCh-}PxS~kwyqDLSC%?;W=($WN0Ha}x;=8Joq3#3`XazufBfC=}Y zbWejC*l!GpZ~L7@%oz>B&Shkww-bT{v4RWhvW2!#wp64dk!f5Dg@s0nq##{`jpDuZ zz(a?zNJq|&WKE@Mw=8tF8W5t|*#hiQs0Gsen?1GzSb7IkHygx^mV9$GM;tJ8LGu$! zpuezQdsdsSoTz*Yk!f_HkN-jS5A**}efP4G-V%Fv#!t2{r_kA; z0C-d5)96gqIiE`%Vx(tc)1QBB_z!wVcdC~5?DMpn8}HK64g50L+evpi=6tIRO1jVg zfIHy7#NDRrF6RD!T#Vh-oRO%x-G5z-;pR}TkK0O--sq18tg@{PO!|(@vf7n#QnyTO z8AcjZElOF+e3{cZsURSW-|F}%aj!~a)ZsebFD)6!gq$isW9ET;jQ)^V7G^Sv#$h7| z5`B`_V6mt4Ue5E{FTh7G-R8$_2?4|DZz{VVgDQPDH9UoyWU!c$cmZcsUkF>ztteof zQ~yq!P7ea=RYq!CK~&{G zH}9itc?E$ESb|abcn0!2dJ~`vOpCydWew?(jRD83!>2b8P$(IG5F_VR)v703hf@js z4p|=RtCg;;1`rY5x3UKW*Ga@AsXc)Ze??CSBxwIEe2#V8sjWzt?~REGTzT@NDr=K3 z2JAOH$!tRv#z{0_(ac7J2w}l@fq2_&&{`L@I0QE0j5a~z^I!R0IAACVJc5&(a zO)3^QBim1p^n<_OGbX+Tw2L8jxfb?OO%5!4x->RQz=uHueg3gLhkL!AffNac44c;u zq8-Gje)9W-vBGX7srZs3&RC}39jo}%%d8kK588Ku>}rLAdi-{)PuDHXYi4S_B-um; zN$|r+fZ{M_We&IdH%ZdzHlWw{BqbN{ed2f#R~{vp(8^ENV~w4~T&w5^;$d8!EY3jg z-FcL7Yh`G40z`VC76GX`L3s({_)h{9voN(95{xfD%O4Vn@2?0%l&>W-iXizzTI-XmwoK zl(1)%oD;!Go&mEfAsuD6_!PqEV39SBMrw|xu7-0nm5?E^Q#J=NZ(|UaDi}4 zfGI_n$^?JfLT5xMS=34`98$K1IMHcAZua!5T}aZmm!24vpfoo{`o*JdJpNYFHu1hF zC#uvVb_f2}Hd$bfr{GbEpLE_=uu-Vm+jaEd6ib@MEReUeEk{u%ZIf(vhua8XPi!oU z;x7%H^*^ys6>c?!z|Fx*b>t4=G}T4}tjbIVz})pfGFB}Jh~?S;So?)!Xo zEBjg*)?S2Dmu7z_KD3E)qDBZoOS!L5A2L7cNzI@ZH{zABTtN|01c<$~BL7MKJ4FUn zaSSYE-VplVSDl|77SYP<3f^^p>mU5`s_cl*iOQ`M}AzRmtgn*0(cfLZ@ zgL_IM9bcV_7e~*KCl`@FlU$N0&_&tF?X~#UMDfoAPcBGbBvP1$s3<^wHC2Aq+by*P zN0Npx$yHU*Ck%gkDI1?t6-zQRRX=Y2TdYH-PH2VzSg(1zDJ!A}Uq8J4$xBbP%99Jv zamvN*F=D<}Lg;TT++K6D0RY$u3Y~xkzf1pEfmUN&3T*(1i0Y6TSm?rN zZ>dWmq&zJk7^0l@n23hCCQSYUtf0AB57^6>D_=a(CjnX5#Sk7;q$2}2rIl2x*ELW! z#%mAYiUvd$e-2OY-o$D6 z*2`0laLPg(vPnx;nYfHTi5V7V-zUB!gKjr*PqO@{Uic2^8xcxabOxODZUAt0jsPa*Z@TqM5gJF zG3ykCW;QLT+YmQ$a(n!Sza#o8WS;V>+K_U!M;9Cd^k>kgAaKEI~@mhalSyYsd?J~Bo zf_94>5GijlHjNFzK@u}+eG<-GknK-E>$xVf8*glSgv3X_U-XFzLZrN6WU{0Ufylym z7;FrLiSI@n51-((hKS%rkB_S;bcw-y;9!pgp!9APARpsc^T$IWO@jsjv*KrhB6a#P zz3F#m#kf&z%JgjQV_6g`UBON83z)(y5LW2_UcYM*6 zZO~)UQ*cDN4k^5AdQmM8HB}^0<&z%$JneWeNMqbeR93Jnx|OvwPmryGG>CnzJ7{o# z3dsji3FQF-9w~CAdS0w}S!R}&P}rhh$$~X~MD8nVo=Zu`(zNfGMJe9JYywh^#99K% zG7Bf$bc+=VO!R41+Y6c~VQ=WPGi>yas|s2D3@DSVtse*{DA55d)V;BU5@U#y=D_ND z=SX+-d{*#XtZ;q;!Fh}Rf5<}s)*MhSmIqes>F=r)QU+<=Oh!d7{W{;sL9W5j8}b4y zh$`-#wx=OJjUO)9^ykBmH6aE;gDZ^P;|(0;@b*)y`D#bNzs+-JNju=%R>vDi6hnas zY7{dvGy#V)ZtQ02iHbH4M}(;Us*K?9xAKAwolvQXK8PvNlM5-*D{8i9O#*`0E*N`a zp~FF{7XHIt&`05v{YC29GVz5 z;Ys~5W(Sqel=cIZUZ*m8#+H5>gecRakUSwe4q90So6+SZf!Cy z#!gJ0TmU2WK%W;!-w+y8fWzHl)aUtyDL_kiNQ5{p+P60%^HY3lq1i%JwX#E-0zMN6 zGgWpuN&+oZPu%xU|}|DNq;u z$#(gRUxh2{0JB8~5c9|_MOQvA)d=)d z1v=}T=$oJYu0&vovNg-#rOx-&<$>Yxb;{`&u;uG6M@4@D$s7^<9xb>S)FY$X>itBd z@oi1O!BoS=wAGLpFgg9&Cl$WdHwOoVtV4qJ?5E(O zrD2{*BATwhZ+a->%xk25m>ZpPj;#dhX-K77q@3K`-yG}b-r zzm(?&gKAwa?^sjudRzoq{j)#dTQzn}FK5AE2JR<(B?G~G!VB2#VQCS;Wq&H=EENVi zn-rM4`146e>5)bqc1WWQj9~>y_p2MJ=$)KA6N?LllNrx&lja+0_x)=jMt3gW8xz`D zA3y92!9UJ_8de0+&i_x@uSLE>wewroEc;*U9qV{uS=l6;$wU3N(HLRglT5Co`cQGh z7e=a8kT?$NZ*QDl{Sg6Jb6S&2MvCz{G+bM1MpY^Sc_x@bRyH{ zQbRS22ir(do>c7f08rZn#uOM@DZ#pmeggNuxZU6B!(g2t=-aUWzGp9ko{gf~R!rIy z$Ia1?W_^3?{ z$*6$A-zJ-Lj{C3lka)9pI!>A1GKQs-nGU>=pVL>*3^D{~D$Mc6r!@Lif=XDR`d1d^->@fD3)GNLRcWK&*%vbJp`K@N4|qXF#^BV#`+7HLSO@LO-Fj+g?>v0#fg9lEP|oJ6tQ2P#u|NM z$X7xefS0C-{Z(%lqe<O|cXxMpr*ID*T!XtNz{1_#-QC>@Zowr$ za0?J5cnG)h?|sg_?Y`6tv{kjL&AG;yeSEzaf?xA1K}g&Z9!C`=^*3GeN`ZSNBfGjc z>}M|GxeP|{ia=qmdTr@9T&1cY%0xGlK!LZKFfUXUFNjwp+3%342)6LM!+^E!1J2YUyG0i-gn3C}ok`Qc|@r z0i9n_w0u|gEYJrmmGrB&s;S8z-0c+DzitW=%Pv#Ht^QXT_QLF?$INx)dRZ2=TYQol zaox==ji#8rAeO+VM5+iiG-W#6uh|;F8je-=_JQwEZ55b8&gjKePorkR}z`8kXKZFRD zSwbb`;Xmb<7_9u#Vs;aP+emzy`SX=0@Csx?e31w|F%5;KE71@?tFT4 zNM_=0@$Kj{@c0|hGw$el_33FaN^j}-lb^Bc#_kL-#8C@wVok->r~b5@@#20!bDVos+zgLFW%|sXXtbF> zK|zPPG=dlymhDXCHa~QJI4g`sQV}kuX7d{)()?-*1i~hevsycbzA_DMFygk44CWH4 zfM^a)d~Ru2skq?1!II&wlmT&d%owIT{i5gA>MSBYgQOwSaWmWR4VR&6x@CLJD8x4R z&GRLEYE@8yBWH+Ucw}ZathVne{h-&TyF{>z>yOGJbKJ3||I-`XFl9Zf2>WUa_L_(R z>%^#yrDFfcq}Is&HD2ZVERCg;pGLkzKg7g#^n?6fdf$Kh4xIJBo9njOW@gseW(QzZ))Nq5Xzw`0@J5LKPeuh#KMtp2-^Pkfm zzl;FJhD+f!zdsq6gAaG^s()62UQfTI+$_ym89n(=Z2Q*ODQq@@7`Zl!Hte@%&z8@L zt#gnLHL*NxD92l<;szh!GX7I_j=vGaIQVe=_)CcS{P2U1?|qdhgz2DX#Eb?8?Nc2l zQv;!;O(yg7_T~}d{tg-)M?7f(M)Z)nJ5M^ImLB&4iJcY6$`r=mLTDiPM|VQfAZFi@ zHJtla1R*?KB>~1r!gIJFY)H*kRXmXjzrm*!TEzC0bf=`y-44A4C=`JJ3=2`QoBsjp zKKkOF-9-c7-ov!@`S#zK{K*`)s?Skw_7-w4=4n9<%>tVV-C~5HhV1YIa;9SIHw^8? zG&ft247-AY_y&+IZS=R%n!zmW^fZ((DLAlnuehaKfNwM}D=u5@qky)G<@M4{+YF4T z7~r!h>OkgFKO<_Ie5Q+@d0c?lzH0?gT;X4#FaH^8M*|A4&{H>=zlZ+zVx{Stlfx`j$D0YNZ5em@Rq-^niUM|XI0G+*|ndQ-v zNkU(=fW^WIh}VniZ^J28>7*a6n`|C-D6~}+%aavg$2M$=pkiBULSi_4CzB~St&DF? zY-iU6#W**%coG$Kg&k`r+}6zz&&v7~f{_^eu4XKm+;1CFnPei&{~JK~!x_oF6Qo30 zhe>7tG|D0WcF51B;)h?4{^`ptkFNv%kLbO#4<|xt1RZC(zeuc4=K24!RzxjVR$|LR z4t_DG6iCrO$kld!^CbyPwf;HI_Qxo2E0>BtCaRmb|K{X{s+?wlp+Vl00-7TS%hE7nEZYb31gh-3cJc?fTcgqym!IjJ%lNb6usTp?}UF32)yHvLzq8hyUH< z!%QZCRqaBOA^M#VKvc+#H=un^&W5Hzt#eAgIiL`*YThE&6XMV)i~-{np-KB+W7XfF zv`^3j?YbJYqx?JN6748&WLZk4bm+Qp@Pq^Ovt56oX6Na?X)mY){~KhJEk@2gut91W zvK>U#C_H1F`rl{a&7vCy`fL9V0cfS3hiCnvDWRS5_=Z7q3%ZnXTueGn4lZ6yv6zgh zXp9nWs$fB+n$#B;t4J@|b7qoCzfK~r*vsMdcl>43evk%Ls!MbP+-S+&GB?-`6D=nP zl_T5kal8)07mJ9bhfQ{}4$Osllm#PD&u%dAMZiqOj#_7fv>2ff9U?5Updc`(yG2>6 z$o^E)@D+4I3OfALkON8D&mb-QSN5{lFsR1iSM-9_H>B`d0}*n@!f}V7h#X{N@Dj(4 z;W>4ZCRg>1XE18}x#l#}@R3tu=K?K|?C?=eKyQsPeYU z^H2m00-w0jbn>CO@1M`JxcbO6of{N~>6nGhJ>j|H6s4emY8nEqa-7JZ;lBH-VEt`ohkhK+VjlxXxf(F$+De=NKv?*{u4{#3Zbjhd@N9_r~=+ z@(BlL5+Gi(%PuS~M9_$mLiuRs{RJnARju*(jp*cU7KiKG^etVaftgnX4P!SFY6aTTWP|hs&C4RegGDc}Y6U{8gy0gjsZBJ# z3E7;<`;BDB%|CAyYXiwLjc#6>Yvl_1Myr>uEv!VQ(=?VAhgFd>86mjluy&rl7z{8jvBcRwUA0FOpYh9(pq~ATdyuh5sL3?p7 zjxC~>W?&Q}->H2sDk?#M?@W{lGJ}I6SAI) zcHXid(Kwzn;%fsJK6k+)$Wa~Hh+e(2HpYec#M+Q@8WoF#o5u=@hs%paf{cp>h?xRq zRT8k6OEnfN#2~f6AV(CYMA@I+uS}K8J^SrHuIkA)<#CIe8Hq(B45Oz?)`P$U+OFNZo1k@jz5sRiakF^; zMF;R7l9sm|wF)vH<>h>j>aTHLVzKY-HKfzCsJ}#=0s2h7{QuepsXwd#x&~{fisKk_ zN+U`qw&aVG-eoq`8AHCh59pKgUFD8mqAFO*hLCtvpyC|j+5CZ%Psr1SOH20+UO!2Q zCeu+DXLAl`l+$7WYwF_@Syl%51$t<%>UlSA?S+aMpE)(QZVD|nL4wKVg$q3HG#g0m zf^ILnAx3>~`WPs80tHZzVEYTZqbSXB>M>LJRJ4Q_Z zt%yDT>Y`Z9dFOBI8*^P%=7FMER}S2wsNtLdTkV(uiy#gVdDZfjq4evfc7h+e1`%y_ zB5AO;`6{{mJmX@E*uC^&o8ycKO*Za1e2|@5g(BL;Gy4~@0|dy{OImD)VpUb`$%WbwP}rEd6Kv{%I%PHqv;wo;m%Oe3onAiy|(z^sAWgIlRCO zl>^m>wV!~cU+&nI5>8N9mHeSKv$Zm3bb~ySIBU7Uk*F!8zJggLD5ZU2-w|xZ-cRQs{j&8Ed)d(PF?&aa9;jE^z1an2$1P23pm{b@P zBRWgsZDNC!c~`%i0mNt&-t0o@hKf#?-+6ZjY$>ljx&ZoF;)h-sDrPGSs3Pn`mj(Tz zhe7*u=p>}&2twqW&&v`05Rl>5sd*GzO4cBhHMRa3i-Qhp$w5 z(n+lALv4y32tr1(F^cM!Q_x&_vSjn0iR@2Jy4|WbGrIX=tBq-x-ju7Wn|}I-eW_$h zLQGsyu|X-6(21DYIU3T5v3^4Jqf1CJwSP(Zqzy^fd#GEQXJP~)5NPO0PHhG~u~LmOg5Ba)HAU1#1Ble3 zH^5orMxm^^KxM9+H&Ve=4c}q`rO_rd)3?eW!i?O_D3L#m$}esi5rq6=`u*AAG+L%q zMHl*`Ht94DF^aW3OYP*bfZY)lc^F$2?n#tGkq$lt`aip+{3(PTuc%#Z!x^1sy=DD@ zPW6I$&XN8S6ctt|h7Jre8Vs{eD@5=SJZGTAUcXewh`_3h&`j=hQ*Jfz)rEg#(;N;m zI6{+PJR2q%ETu3;14ZgXn3z~BYjvsIb{4)b2&-0&&;np297{0Jq7I4Yk2;HPr{W@8 zc-t&v`V^*ljw=gNQBadS=XK2(*zm+EmB>s?FPLu_71N3)o?$b=OB)Pq@$h-?YygxK zZo-II>81(k&EhkzwLG7;SuE5$)HUh=T3K5vybndB5emm-`9oV&TOgmjO`ti zKvcX`_0r|4g##e;R>oohlMj2{DLI?!^uobap*Z&y;661qcT}r{Y)dQQut|Yn55^3? z;4AF!RzYpRZB=um+Pi_?{|w^OuY2JAdk*| zTK1CAz@LniGHh?+S!dp~LR`Z|r*mogMO96^eaYLeuH>O>y=`7b?$SN!XMRjV_lc8j zcia6zQOH%G<~+{PeI?ni=l92gg=%+`BNx}5odra(f{!dtudh~Aj zGL8`Am8=weA*0r!G=c)PD%K|887PYZ3f!mRvJh_d7NPo2t_D+ z!Gm)nO@?B=5)?a39Gn!UffK6dgd!B>Ew2QKq!>$roPZsR?6w$d^%9{`=#=J3rh}72 zo}8OvmW-Qq9KTk&1iV>Z5`sG_2Sx1?v<<|@KYVdxk%?x*Z9BpNpDHFKmB(Jm$3q;` zRNtsVTaDD_@5JO^{i%2G2x(Q^k;Y@EQWgv;(|aPflz?pkm?jLc9`#_oPoqLT4s!k+ z#PADfEf?CANg0=Uu~Vf<*iRHWKX&WDG63exM3pGwHKdkoL#@Hnq9H~I|7tYfJrHhh za2(iZTX~+C*9JR}>ScJNiu!~3`-Ned<}1f!S_dAu=4NNN>1ZGUdf@boDQqiGN$pGzja4Octm_r9)09DFUbm(Z z#?LeF8%|)gwRLin)(V8mV8Br@^GRKzDx^cKM2Qu5ZQZOQR5H=zXB1LReXxn!w#1Zd z7Lre0{&bA0vgV=IEkO^Kr|mUUo8*({pi+4}JOghXXJNR8{e8nZ$Vc}-r4Ao^B zoQ@({4Az))c*zPEmsbOc*n$k&{9q5ds?pAMW{Y&X&1+caqO*-$>|aNrt>Q*`5SqH; zEV5|6haztM)-Hw^!hrek43Dv!1ir5)2(mLoq182o5P{0U&Y0De4{RNY?u6W?5VLm( zL^Y4-Zbj^aT^#7^XvUp8bM*imYa3XlV+edba<{{L_{}cHK76`<}#N7s@9?ZUSP* ziuM;&FSDg#`JkL)ij5P&K%0?Ni@6ERx(;afQ`NwLjikd!WYEqE03PkvXrJix3clG& zG5cSA5k0XDhC!u|PwK%+5I~7-1Fx3h6gg=!a}Eqk@+aX-B~j3issioNowTvV4_u+I z8S2PGt(bLFNXwrQZbV=VIM|<{wyI1nDGycOTbWL!3SFMt*-b?jDC6j##gcIqogW2m zKn$|rHXo&T^?nkF9c)E3{+ZsSYzj)(<6X0fheD9%0Px^Ig9!f!e+<*3{9TJMCwo5# ze8;W(TOUZR_J6GPAd6@)oIt%JY0b@+5{#!|Pq`NY{WdQxEnAI;g!D}2g=mn$XRU}{ zmO1Sl+o(TX$zAF%TX-TdWj{=N^Zse~~ecKa*GtsN}gj*ZL49%s%#&qIu+-LWcpkXqiPoV0`P zTPOEh1H1+b6aQNSNZ>V~b4n^QWf$fq#cf8G7YIo*4|bCe3r7;3=7TO?6APolBR=eO_Rk*9@UR)#%|A z6{>k>QLy!+f*ihfSWV44;T2oQ$7|vBH#mxh|8VB^!5~+?bc?@pSF``~_N!y&;CPv>%gsc^4$y2=;N!oM>)BhN9>y!-RO?t9JJyBTFYCXLqdCrdA?IRBtO?s(V?&Yz+; zQ$8f96oF9|dC4=lSstpy6En+YQZ>-*P+cJ6013qce&iG(tYfOM5m<%Aut;0ylEV(+ z>=5HW>@V>xZKwG*6CyHf&MlCJew8rQ03+LO5C|{fB+{kWVgYq47RBsL$eU^+;#Qc@ zH#=cUa36E|U0kGhUAlN*E;&1!UF~78L=hGhX*aDbfn; zfuwMxuPq_i)q_Gi2Ar(A#%^eF)18iT`$+UEJwo2FJsTZ_#f=i+tH$~yNmCgYrhnhN zkcq(LHm|*z_n=pW7;5HD-Z+%SW|?9qadaaZn#!@}tncBWNu|gZj^!prq)lD_g~q~+ zMSxQGstewN>7G1G!5C;p(ab0@FoEd1`rs{c4UvBYEr2utc%xMAMVaG%hC5eaDpagu zQkQ6J(gO(yz*d4|wk&I~S#*+=zm2wVH4)FY5TI@52=H@Sb*`C3^#?{FPH3;$u4qPV zX3nw8^6uL53L`faLuy_5uwd)8a=oGo%v5{CKT9$0x`5v>HoNnB1yTyqy} z?^s4Z5H|^)&P!!1SP=NsPp?dm)Dy{)+Nwhz4EiZTuIr*Y$4Y`z_7O-v1iRv=+#g%N z@)W7M%_L9=(kslF*Zv++au;YyVv&p#eEDQUDOAQYeukUO!4+NHsTW0qaSUe$7$>+s z=4ns{ygl3FmGrybr3G7eQFmn|5UJi67?x&D3NFsMa}JPbw0)2oE?s~o>;;f}7h_ob~ik4rvJ z{L2!_?^+X(_Pihq5}dt}I_M7Pmc=(ybS-YXgLBC78t_P*>zsTn22`XTP&XM+bz{q6B zq35Nf#sn6*(=@0m+tj-_H^s?1T{|8We zFU8KX@NYEgkc-RJ0mi48;uL}PuX-$>XRM>YG+OwVak~_mH9}E>sHH4P1+(Tfpp>M= z?-2?EPrLFhz-O!yg{vW?$eflrv<+R+gNx2CAo;&^g{swG7KTrTzTY`CJOtRaTSp0K z=i-{n2!e}VP^=WETE6eeRBacK$wnBBx(;Ej87Dp2Z+8P=xS>D^=Ex_cX)J1vS)CX| zF>|4x&Lhe+&Ba!GYM9Yn95J%&r^r7--M&E^#BheZOKy6ptz{$-e92?)eATbT78>DSDEBR>bEsC@F<@g-!{#i|q;!vHA+8X35kg^|vna-P8c(=(R0tEN<|)9lv;i z3{v=?W5*I#e`VcY88HgCq}Ze-=@|JdqzNfrS4@MiSga1-AO!5Q(@5G%$Hl&5V4X@= zT2nrca4vFBLK*H->>yhk z8`lmnM@elC+`ahbD(Q;kVXPX8_$#R=L4*%pkh1L+21zcsv@e`TW>|%a(TxRT0ko4B zOL(}G0Z|Ne$N1G0$yMMZh?W@OSh}*u#+X`#T~h8i>=UUTW5!Dlr`mN2c~!epN*JHp z_r)HYEK{+A?+4Vyw(Bq!NX<8}yh;g7h+{Xdxcy}x)Cr**s2rh)J0%)qp<|F`7f zb5b~9)Ocg9s2a%i^{$r%s2HJ&5ICm5P(!%HFA24$rv7!hnfX!VQJ{s+Zt%Vs<4Afi zP@OuVuqj%-#OcA4(1a3svfl&qK%Z!rxI`q086u+SK|(|ci$E7_qJ$8aVst{ni-CHR z;$S<&6b!WTBp+vs17&5;4^kY3I@3w|Jf0GIX})>Qv1wKc@# zKhyvYMy?)-r{Os9^W4Ngf>nxiQZ8Invednoopl~m4R{N zj#HGT)%3K)#;vcBpDOm~?aWG@G7awq23Njtow#xgkl0^--E~{muzWg6g-=S&o5&1) zn6uE;So+la?byy1q8?7MgCZ$k3v-j2WcA}*CcueUCb?KUSqX5}7$k)ihZ1S+eYoEeQ0*S~KOfpf}oqyEV4$PKA8~w9m zgPOaVAwBfuViU|p!kR)D#6TXr^n0bb`Wgn9BqF0k(yK}9MQ&UzoFuwM@U&_%0|$U8 z%Eu&3My7741PX)EL#PYnpP%EtG0Vqi9fl41Lf`KY2HY>ehP;dldWd_nQr7yiolUiXZ%KWI0aJF zF+zS2_B+s6Y4m-Xo7iz_vw=~>ne{fd%j928eB?HC)5+`$vd=?nqG(?_C)^YAF|yU6_}ohv2OW`jk8s%ej)MFW{k-f9t$tTvw$=I~@Nk10ccUtPAE& zBfxaQ;Ux|=2@N6)Hbpv0`$ER!^2qgHU?$x$FV|8i;1l$fAxvoVOeb;gXtm;)o3Z`M zM^dt&w$uYXL8JYb#kL-(N1&G2G^|G;1Vfd0q$4$j)MHLCY+4M9f)E1SfDfg+dfR3k zGsn(wh#?C>!={!>xSFU#D0#-F(6>L)LR^zaYDZcTLW*c4)xZ9RnbbESKv`^~D#w81 z_T~)d$aIsH&9TWsuqzH@DtG55UuBLpi^k!}6e602L932s1u5i9frcz6BzZm~22W%v1xr!>nlPy!|zcQzR&8vJZvtTuza z$Co{wq8bmiS3HDJ(|K(2OXnTXM>}Va4Y5ixYv1~Kslw3b%Tt6I$<0v*KDC041g0PV zPj6?io<3K6ExfV%ABaPc=$)9SYfWl;>(2UX_cWL|gl+A$r(*oJczb*NyT4L*aF zSB$GPgl~TBV54Ft+@zp_8-@6w)19ITmAHfg(YMO;MQ?(oC=9{W4^=subz!20)x2}V z!HfiY7WyTIE*o0rFetSppSq1XP%KLEfHXFtoe9M@lu#N|mrJp+T?7I{QL_nij~i@F zgj=R<<>SC4LJi{D`cj5#D7sY!GbrU+vLbWQ!=L|UGw7nT@+ea|T5Ji~>izF{jHCM> zP4jp3^rib4QMiBi|4nB&@c6$`Jltw;Eo&ZM+JJxY3>4Aw+RWQ-ejCo-{IB0{h3s*; zQ=}rF3-DTh?Sl~-G<;p;Tn08mZ5Hv*5xL-D+f>bui+VC(n}A@ux9jt|NJYZDtoe&m z6F9Y$|MWk)CTRpr*U-JK_WJoH^}cG>rRX*1esD%CMm!SWSHmhfO5W_5aN_72!bt)| zJf>DpCp7Ono%qu82Y!8Go;F?nHzq?SRV2N*j+|->tV`s{fK1 z+-cbs+UV}8wV>~@!dOFFdw!+Kq)Qflu0Wc(>oY=eb)P=m$i`KbWuh`776(~Lm*LA+Tewcum@O^G!we{hPlT?b~0vU=FnP^thiSKX}>kY~2+IMK^b_Qvh2;V{~H8 zHB;3nbJmxMO{*Om{&JpeLb}8`d8fDabq!-^b)`kd&i3qiH-`(Q#y?{$-2mh=xxIK# z;ES{rUaYGDf+urRnJFIg`Prn^sd!6lLsgGi>YF%1Q&qyzeu^XB7mK*e4WeN-43iR( zVEE8w4|3`v=wdr?CY||AA$|4hANlQy?a4VGt#)@1(>u$E-Af@9mA&5(?0V)jVHdQo{zjNzvQD}_n%&zyk* zd3=N@zN#w$*Cf5O5{wU8oqCbVn#%+|V{illzA$mxot-bmv9y==`d_?KZxVj=KAZTw z{)62HR$qtWWB9~6Jqs%>wA=fSlBKj0i((R!{O&%+cO3n01T5%^RpLuOh2TTi4C`wOeB+$`=s9U ziFD3v`C6kR%yfALRdxBqCAsS)vE-kWsv1@2)S>i|1`SgAnQ=1Dn9IqlV7=c?T}iPG zWX_S2TbAykt^{^#u}!ivV*PZ2k-&j&vn<`JRbvG-x{K2>(mK8r^Lgmz6{R~Q+$Zz3 zl@>=NLes*w{<}=-*}V-dY0)3$J+U}T2?lXlGwJ6ngBtyz;aCZVmE8H(GzM4Ejdm>w z41h(S&&v}DuiZ++b9p-Yz&X<)dc8Or!I&!DXwqn#RRvB&sm zoIonGTy3^N7LLL)(*7fL@-B2`AVxrfOB31oiUo#s^v^1k;XlKoroNSUjr9%);_q>P&en1L}J$T4UFegblUaSK2KF25d1yJG_!r)L(CkMsY5b*n7DPD4z%N z>G*zb>vT{>Y7xc+1+9 z;q=JWLal{ZaCpnUA0O;|An}0Z6*E}^j1wYRQ=JS~^Er(!gAa!fpo^m$Zpi;zLJR@i|oedb9%v2qhEj&9K!)2zBN(v+ht+#usn9S&%stF4NG$<(T# z6b5~$sgzalk}7?|E;xmJSiZ8r@#<{Aq&A$f4*vv$mtR}eIT+E)@tK>C!(M5nWSiAg z9Q;dDd(e7nx)GR=1h~@vo?z^XNky8A3BN&xf%PZZnROpRMwxP;=BUL_40(Bj4syi` z(gZ84rcw}%T>|{do9+U+Izbl+Od)N7XAFhe&;!To#I`v`1|;7 z`(Ww#@~VpeGwr{TTE^eEoQ|B6-!~fwC))=V^Y;`$dPz-z^V095p(jsPiPneCf94c1 zX+sWW4;A$U_E#70g3W>{rD^?5u*HNk<@cu_tp5v|DrF&*H^v#1JPiDYjFeY&ZZSlE zuvc!HiQmbm?fI?b>!@V<&43z8&j%faDqUu}7(q}5aj9MgvBMoovJB_9qYUT21nbU7>XNaKlYF%O5c-h|hF4rIHTNENCaO77T$RVn30t8P z#~SmLKX% zmTV-%DJXY7X$$CdT1b*mhF z|8t}9OK(T;y4T*458w<0ypD(iy|s6IJywNvJNnv@Y(^b=60^}e7=lAK7V&q{?Vs~w z;>}xKcCgM0=H@;+>2xmWo*UU+V#rPOZYqr93%hNbpLEN}staQ5pJ?@}7s{_^Lt*wrlQ!!eq4D}e59Cp~fgR5~O4m$1SYocm0Lk{6=qnBJ=A4v4+{(LEB^5DnSynS~bpdDB^w0*qcl|<;{t2hFF99p=1DuJ;Gr|tXV z0c=~EEWF|7Yy#z{oWgn%==ze8NS?+dW?>({*V%#}xs_t9VbPooD* zBPGE3)$!Zw_deimuZ0VC@x7Su&nG7j*!eZZ9hvZ>Q*~!+ z4cO~{IRm!(o_wAJUl&-7Z({fmyZQn?*N3v{cErC;_`R-MFnyPr_Sg|1f_y z@bl2-@=Ktro5#)-f|dLlSDbwBg1X9P;uo#uU(A4*Klqg_O7Mkt1%>kpSFqA+o$SQg za@F z<))j}pypoE^Y!Y$ZyP9US>jZOO`hsGws;`{e5q zH~=b2PL%dj2$B4pF99Yzs^$3WCYbC{(8%m43HD$dG{sF?<=THk=1^9Ll{Akss6kLR zhNa{#urlhptEreY)QhD+I*8}RQB_j$dlVn&dO3f){ChsSQb10Y|4~VWCtcm9Dsn-~ z%`Exzb)OQOe1V~ADCmQRvvwKVue;zQ4nQK-fRD#S@Y@bh=gPXa-))R!f%eRLdcepin!p0DkdDrOsMGz9VZMw>dPu0ndRoenN zOFhmaamyib^EBGkQ`dtM%BjV{b_cD)R$G!LO9}S*g>}P1{YXJ+cLzD#PgqPPA04tAxb-_J2lbqCEJrWxEegjE2;gUL`EdT5?3=XE zmXLgUM@bmZI6U@@pp&L_I2^){t@lqq0!=L$+<9020FSMXn~vBNe;-942$ar+w#kG2 z)P{92Ih8dxF(7B?ks$g>M1<9FoPitC)3WtFO3cX&sgP}aul?3cpC1ZPlnDd=7LQjJ zkJmq_2!f~>S2#(MCyt5C-BEN4ju%O6n!f#>eRz7{8++_%j#TY*7g=A-r?o8Y4 zkwWu7V}y)x!8eGk+2}H(qw}*z3l3vg^h2aseN4`||k+@h-#}Lw_fFXZ}@n&3~?VK`$61o8s&)AJs{!W5CqS|Rg%{TCf*TlElSy9qG5JN>#W&K-#P4ONt z+xf0Vx3-?RogrwY*d*HjyxKoDm&UKh6WJq zl6v2DUGt3UZU)>(ce4c?=R=*ZL6&^m_5N}MkCy|v9 zZoJ$Bi9nw_>`^gu7rx+`=Ta(0+f9T@rp@cbz^&jbm?8h#ukG;G*ui~Sd%(^^Wx^8MO(i>$H9dYw z4Bqm{$hjohynKKXHSnr=GW%!|^hEjLW0d$wm;1FR{oE_oM}7igZn+xv&~n1oI^$hj#Z~{s{f7b{*I4;aa-04+14^jKc5csFdEr_ghY&C zw&GWZ&yy}Rjyo|iFd;MlZI6|ac|%mXwlxCb3^I6a7nWGsn4>LH>j0_#iHGp|5cC)BN1Xqar7F~;GR9f zXNAPx-^^LFk%bgbH1HFz33U@S4Lk$7_o#?{T~NYXD6R%5g~Ym5e+ z(KQuD9nTE_RjGq81Mz9qRguorAmw)@(Nz2GOQ~!#9R&?69Mr5L>ux3} z4tm&|30dD6`x8(7F~ek?I)qeJ7%7HAx7jrYzjJABq25};ykPh2+sHK)(j}R43AXO| zmt^XgY7j|Oa%^lPH>9gWmu!)yxYW{~=j;j6)Dxwtkabqq6 zkuSDG|K2+lM-p>TTT{}&1#vaO3hmlL^%5e(G)p`j7QHZO7T*V^!F=A8+stD5F3gT#rjP*p)mq z6bw9vj9mG#xRVHKF+fc^(aw!pm?Ee*9NXgr2Wqlne0E^}rIO;_I#IJ`G~sU%>SC;C z7yF8;J2-%adBVL6p{px#2=Pf*$IVLQolAu7eF{3i^%vn3Z2d|eN~q=7PJ(s{LWZ=k zpqGyku5DKQ`2}NwuMtWETamDKZk)s~O9~(qN@~q7Znz>!nk#1kas2=;i4o(=Vcs_e z=H&hCG3!=N_F|}7PEt-b>7~P)%-LNS8qX34+_2&)Mj=#YtQMGZ795Ey%8pgNaTm09 zqBw|;6jdr6Y0j7Nt*$Yq8Q^bsmBD2>cM_+H8WE0kkSwP`V-PZw4Z+5Nmq62d&jTQ% zp?Kn=+>0~(-7j+7c2~`%D)HyYRdFnZ8E@siVymb5?K@s1o_`l$9!*ohXkUZJ#fb0Bu~Y<~RJGZ>QUiX$yw* z_*m4lw_<%W%}(ZQ5xe0@5RhMc$IzpOr;{yiHR zIoMtyo#gEnmb&{3=N)=9Q26yd_`^H$JG|^IQ?_?3-n*$Pe-BM1d$dLfg*Ps6cu8Pu z;2GJ@^W5bTsZgDHzeZ_ubw4+ZhB%A39#3tdK-j9`v@NOOSbfumK<>nzCb^aAAX(W6 zm=y*-VC13NVHQ`*V93DPV7%cXpu9*Q)P;Zuen*&pF#N5D^JbP@`F-alWcXR9oUUc= zm=7VGgBmA3@EFB5L}rUiVBsEsk9TK=p#NYa7uFe@^k$#@S!q3d0n0V}aLsHH2L3w43jrSYd zGtzVQOHC^oZ{&SC zCev##vuI`|pVj1GRbXpPg3R~i3qib?m_d>9$zv9jIW@Qf;qQ?uNSUDImq8W7anv5$ z(^x}{Y~mThnCtzHHA@0&VW(leMyD?nEy9axT$q{-hmcKmzmLQUT-XISpQIaQWnL&2 zoGWzhpw&f>p`$R7)3j02rcyN!PLo{ysr)+(Lx^{@mk5oZ173#wB7MR8@a`64$I;|A zb8~%qpw;5g675vZY9*d&DHKly~;=X)R}@>{q8a>nDA$3^rFNpCe}F&N8vRJNX6*9yDu-not2TQ%^b7YU-`3f>t}LbP1{wb>!29# zPb{(k+hSXzTwSau{1J0v9Kk{5?|yM#!4{hWaxOmnZ0a--wHp35>rED(h?)=6N>r`s zi8=-8EYcs}HuR3iV^!Ae-^WKTVbjZC>BkcJ^6w`DB;f7qdM?k;juT4y(kVZCKhER1 zTwSs0Ic={W;=t5hhk9u>gnGMOG7L>bO50Ea(u&POW(2j+ULbcCg{$JV^|_3>x`{Q{hG$%3tlt!Ve$Jv8IG0X{xm8XemWR%CtD`h=bPu8wf7_L#c zhmd#KLkt~G!gNK)8Yq@FWsy^i#)%07M$jnqz~%c8BDjsQ5&_gGA7-|45Mml{gqvlY zvP=#Yyy6xYj7*^izHAo4YRBtILj$n+gD;V#?)#s{$JY~sM@mw^rVW31@r&=eCKtv5 zkB{5TA65PeeYx(F$@HG*M}LfnAa@oE_}stS&+6}z+sb*o-3gKhwW|FoT4n^y6Cajv zMp6(BmyzObQpQ3bhN!S?_tBj&a>UGA+8j7*ZwnFV@*G9x@u=B+w*L0)8RXH%94VNK zG1lg?g!?z^c}WWz#Oljv3bpv9b6Znb)q+oes?kD5HD z7jb>;Rn*X?mvLDl2Hz5B81(^^kOq`6GSe%^{xLK7^)DT8ctOhjy+m3TocmJhD%OuH zWyNy@!FG$IC>$?NnEd9O{@=Ts(K=rH|7_-bVlEF(^7?TAFW3CafElmNh${r5OkS3ul##2-Nl;r>!h2c4V9m140x5fzKV! zMBq>UwoPOR)BIQGi!jFdyFX>wV&ew@p5whdh7kNGDyNyR?K^nnuSW;>M%2H!TEo0a zWuFe+$t_oK{&W^?N{6D|mQ4Imj_-&8~lUz5DyTv`$5913Hp5dwmtAi?f>EFox}V1zOLceX>8kRoHTZ0 z+jbhWX(qO9v$1X4cG4J))A;>-fA4eMf6X;KduGm@v-a9+?_mf`{W}AEyYp~gq8Ly{t7 zrlzs{ngdJjMWNLe8EL+5VQnH8(? z{vv#nA^Jf?JGF7cz}eI=k%UsXappR`H2WiAd=yd*UbPIpauHmO*2-$-EIT>EM6RVu zj`k=S-oMqss+6)nYSB68f?e3tCde`nAy_f|D2e>a1rwSIl3*Rkernjzx3TgFQ|`-? z<=Ey`ifbv9ZOmj#>u*SB+wOc9M6o}_f}G~blspZjXj*+J5r|03YiGH1sxC_yV(4bo9SEF|RKuO*!^=e9)%>2x>9 zvd8B8wlU1T$qIuQ`3fi{LIb$I>r&J7J{Thr&I9}8ngkQq^#U4v)1k9&8T9q#T3XL} zOnn~+oZ&v`DY~3E80}!vP;(WMe^&)s=}Ow%mpd*`d%}aehoes(Z1JN{!|4^%M)>GH z2v}p>69#_(!rX$v#|(2>46R~6qo)*GN;%=ypSWeK;;la^N(-WzVA@(-=*8+{#>>lK zg)bI-aaA7L{KHtv_1!+_c+qQrr&QET$eIgo2DOi4;?wC~6bF-og$TxaU3sEqm1PX! zsgZ6u$Bt@Et&_&%pf=nHClVOw?`PAr%?l^#XEPN`pj)g%p4SX2T5*UUjNaSaPdenj zs^Kz6J0Tq27at)hzYlY6SkQNDbiw#3h#3u_uP48kV-^VbjOuwN-&6l?4XSEnQ5}iG zt|)n8p3l=i@y|v&K=}NZp)99V^jo_tbOvm%94%+*#3q}8^PhW!Tr0h@S5iTv+*EQb z#MmhrsFQaPFcn|@u{dBVP6#in`&i4_?GDH|IuZSiA);OUoYh_XMsq@o?GSq#5nvC{ za=8g8s+zGOW}Mn=Wh~JdW87ohDck2eLGea%8k#CYMU~t^j@n?}OW_mt27I$!R35Hm460Rgl+{~gn3DDS!szg>s zN`+8i9NLQMgJQ9vWUxs?86@-?3u0v?ceh>xQr0QHCteN00CT5gycO`kSyrgLT1k4GKAha8oD%STf4O{GIHwGajV?+W6{TYhJLNC%9vKXd30bqW zbH>pK^H0fWP*-Qg%S)m)G|nWK_SxfJxj-^2O}(&FVi=oJOtK0ByqI|cY$($*R2lMd zo${^olUrToJ}IoIa2$we7xI#mn$Wj2WPEBhZXaKJd|D`g9D}cPzZ@y2D24RGRKhT$ zQVp$avVQebKN)kvjYMR&oh->4lfcZ%M$vTXk!O>9O=3sQ5)5b%C>~7R-LQ_r9d28d zvZI1a1lJk6(qn1^tQhj8@T5C6M921Mlw_De6F^BzVG_sp@xw$mUX>WUVe%uEJ^sH& zG$>xX$W$YAA+E)quk~jd!4T-2rs))Ns|~S~13{*XF>pb6aVd*F(Gp0s*maz>a2UA9 zrW}-RZX0sC#bMdvQ3PE(#v~M$EnTSMqADu&z2NcAx=^t|ShFE}!{t+Cn_Z8gc;ow# zG6P@E(nW#9l0ofza|R#gcI##8pJ$hly-xZphP8>dNIlrKQ!+jRj!$ow1fwt+@-Ih* zRHvIqhWRqfJ8!To9|oOndE$XLE&OzA5O@Q2e1&8(?Ig;?IJ%KuJ!&T`{NhF03S#2?kr(aN|H zl5eEmUJKs5$0Yilf1}p9Sk9}`EAdDYLpbqb$6I7XL@2YjOz zg^osJPtywuuIc-?)sWY*X&aj(*A`^N9SM~Hdr#2#gWS9qC`nFThwOsBz;)rl(AO>M z=%Q~-a`~N9lxH8N*a-8A?U##V!xCMHs#b`$6&!e?;~giqa4#jlkb7ka?tLS|&sr8K z7gO;Lb)JrBocF^ZQcE|^KKd+lXFqiwByYRWAU9Uzg>?A=m*l9t8yU$zjj>kl97wjb zW=lf=W&yXsXau?gF800=?l3OWfPlFr=Zm4ofTMxnOue{intuXX1;*(mEtP$WF>kzi z+DIdk>|ee|!ssSES5hUO{be&F43LHFtPO6bL8_}kkO8n-fuM&t3%0aktWh)va}(WsPM@nV3(4b-?Ni!=$3n!vc%THmGmF|5elQkb0O#5v;l& z5zzMt%Ujf@VtM{mA_;dzj=AAOr{u&J>fGU=3R1$G&qgZpfjcv4ZrLz~HOBgVY5@>R zs-Y=1vzHc%;3yTp^e76O#fz_pm)tGc7;kx8&!;cxS0R2_zwFKNp-gZ2HVL)4+VBR& z9yP(0M9pKdxazwzCT`b*c=B&M1h;Q#@ivWb1HfUKn-Gl@9yauXz)59?`5h;cq{s<& z;E>Ua=8u&0y~`caaeNmo(zV=LN)uQ#o9s-g;gDjO-q%44#RN{Ge)F`}tjf{Ui_dJzIOsj#8o&}kbPTLo|jP%z%{ z^O``Zkn(kzS#qu$JfUV4OL;i<{t|?J^fO%|hClZ>TimWqRK6&=07ybjyh`9ooq z_AKwsdbS~v&{Hlt*pD1-DaH$qRb~2YzZ8m!zO3F(aw*szpp}b@dJzr^*C%XcpgOY8 ziQhF+^CG7Vi5);+R~*@jWB^{RwkDeP!gV6<=Ho2J5WP^S6)Co${7=BE_nchAUJHD1 z9Gzi`noJ{Mm{OzLsFB>O5~6~W1&<``;$mJ&ngg!`T{CIOMpBpfvK zdyG0=U8)mI1;n&tGhwq2F=^Ea$Q;^+&_~tFd?2Ixv#>LtTt4;hARmf?(i(ftsKM4qQtIaJ3{DwwQbtL#Nc!}1$CwOSdj#NRW)m^`bAMd{h> z=^3pG>TR#Oy&1Wn;4S*~`QrHM^X>#l?N>84m6|@M{i8Ii1U&G$xdjUoFn%brz)ler zgA!N$?KY1B{1x=hL`S)j#x5(s;EK@>z3kv99vz$RiGxZ*ud^D#Wkp;q6c4Grko}1A zZ$b6rRLwYPP_qRKAfM+!j;}pVvZ#1tr1QXAGbsX{(}_U&89V7O7~0Q+#~a1Lc?_-td!$?gewmu+TE{3>{rZ3_&6v0d?nA0~P{1RZnAq$^b{ z)RFcIJ1ib~x93xCur8;ip&Rc@3sWRi zfpq&G9-mQY9UUD;yiF9DMa}9?qwZ=AK{bZ0rgG#*z(Xa`0r_V>R76t&uNM0S+?AHe zPO2ovpt?auqND6B$2nVj4y;no*nr$IO)ODdk?$FtrhX~2hP>8kiCc9>tzpk**W1=g?hEd#|v2a@f*_F5R+v%X2$=8kb?`jM4v;#BSPMPd+`n4h~^rS{*37 zg?x3>Ko?|qS8y&Jm6rFsknKNi+LPtnD;G~#01Bu({dKPYgEr=~qB1&Ry$yJzA8#)Y z8-GhR#lDabNTZCgOVT#$`6)Zt)SH1}#y`rJX)HxK`;jN)(n->bH>MK7gcr%Qxq@VmjDh4T8sg%*34@LM@2e`&S zxd_^6JAAQ5My)*qdE13!e;ky6S=_3fBFbEMD@;D_oE{cg0x8wMEdATC32^dY@_P0w z8XOCl7pZ|NNOS!A_yoZO?dao$@1y)LaN%XK{}j*`!wNW0 zK&>cGm>Q_&cbgkax@4J5$oqHnh8%@c0La~G!47PY?I?;+K*e6G8<50F&z*5 z)9?a#=$s4M0{k+${v`GTVUdee!9;kZyVQyjCK05=oflFt0Sn(9xo{m+7d1Uh5}9B} zV9xUHv9d0qnp|hWqbe3qczcD3^2#ol^#YM{Nk?%{z=-HEl(ko&on(21uc+g+$j