jump to navigation

Manifold Air Pressure Measurement 2010/03/27

Posted by Michael in 2JZduino.
add a comment

The intake manifold air pressure identifies the gas pressure at the beginning of the compression stroke of the engine. Accurate measurement of this parameter is critical to determining the amount of ignition timing delay necessary to safely avoid detonation.

The pressure sensor selected for my 2JZduino is the Freescale Semiconductor MPX4250AP. This sensor provides absolute pressure measurement from vacuum up to 250 kPa and is suited for automotive applications. Mouser sells this sensor for $11.

The sensor is mounted directly to the 2JZduino shield PCB (seen far right in the picture below) where it is electrically connected to +5V, ground, and one of the analog inputs. A 1/4″ pneumatic air line connects between the sensor and the engine intake manifold (“T”ed into a vacuum line).

Sampling the pressure sensor signal (as well as the air fuel ratios) is done continuously by the Arduino Mega on-board analog digital converter (ADC). The result is serviced by the “ADC Conversion Complete” interrupt vector using the code listed below. (Note the code shown also services the air/fuel ratio bank1 and bank2 signals.) The conversion is done with the ADC prescaler set to 128 (the maximum setting). All things accounted for (conversion time, interrupt routine execution, multiplexed sampling) the Manifold Air Pressure is measured approximately every 300 microseconds. (At 7000 RPM, one engine revolution takes 8500 microseconds.)

void setup()
{
  // setup ADC
  ADMUX = B01100000;  // default to AVCC VRef, ADC Left Adjust, and ADC channel 0
  ADCSRB = B00000000; // Analog Input bank 1
  ADCSRA = B11001111; // ADC enable, ADC start, manual trigger mode, ADC interrupt enable, prescaler = 128
}

ISR(ADC_vect) { // Analog->Digital Conversion Complete
  // Channel0: MAP, Channel1: AFRb1, Channel2: AFRb2
  static byte SeqIndex = 0;
  char ADCch = ADMUX & B00000111;  // extract the channel of the ADC result

  byte StdSequence[3] = {0,1,2};
  SeqIndex++;
  if (SeqIndex >= 3) SeqIndex = 0; // constrain SeqIndex
  ADMUX = (ADMUX & B11100000) + StdSequence[SeqIndex]; // set next ADC channel
  ADCSRA = B11001111;  // manually trigger the next ADC, ADC enable, ADC start, manual trigger mode, ADC interrupt enable, prescaler = 128
  }
  
  // process the ADCch data (use the Left Adjusted ADC High Byte)
  if (ADCch == 0) {
    ManifoldAirPressure = constrain(15 + ADCH, 0, 255);  // MPX4250 sensitivity 20mV/kPa.  15 kPa offset
    MAPindex = constrain((ManifoldAirPressure - 80) >> 2, 0, BoostPressureIntervals_LookupTable-1);  // index: 0->25 = kPa: 80-180
  }
  else if (ADCch == 1) {
    AirFuelRatioB1_ADCH = ADCH;
  }
  else if (ADCch == 2) {
    AirFuelRatioB2_ADCH = ADCH;
  }
}

Note that the value stored in “ManifoldAirPressure” has units of kPa. By coincidence the calculation is extremely simple. Firstly, there is a -15 kPa bias in the sensor (calibrated at atmospheric pressure) so a 15 kPa offset is introduced. Second, only the high byte of the ADC result is used providing 8-bit precision for the 5V Vcc reference. This provides a resolution of 19.6 mV (5V/255). The MPX4250 has a published sensitivity of 20mV/kPa for a supply voltage of 5.1V. At 5.0V supply the sensitivity is 20*5/5.1 = 19.6mV/kPa. Thus there is a practically exact 1:1 relation between the ADC result and the pressure in kPa.

The “MapIndex” variable is simply a lower resolution expression of the Manifold Air Pressure. It stores an array index value between 0 and 25 that represents the MAP in the range of 80-180 kPa. The practicality of this is that in this specific application ignition timing only needs adjustment when the MAP approaches or exceeds atmospheric pressure, and a resolution of 4 kPa is more than sufficient.

Engine Speed Measurement 2010/03/26

Posted by Michael in 2JZduino.
add a comment

[Note: This post is obsoleted by an alternate RPM measurement method using only the crank sensor. See post from June 6, 2010.]

Prior to adjusting fuel injection or ignition timing parameters of a running engine, the present operating conditions must be determined through measurement.  For a forced-induction application the most important parameters are engine rotation speed, and the air pressure in the intake manifold.  Together these parameters indicate the rate of air mass flowing through the engine, and the pressure of the combustion chamber prior to the compression stroke of the engine.  Higher combustion chamber pressures relate to delayed ignition timing to avoid detonation (higher pressure mixtures burn faster).

Engine RPM measurement is done in time with every third cam sensor signal (three cam signals arrive per full cycle of the 2JZ-GE engine).  The code used to calculate the engine speed is given below.

void setup()
{
  // Timer5 Control and Interrupts: : Cam Input Capture
  TCCR5A = 0;  // Set operating mode to Normal
  TCCR5B = B11000010;  // Input Capture Noise Canceler = ON, Input Capture Edge Select = RISING, Prescaler = clk/8
  TIMSK5 = B00000001; // enable overflow interrupt
  TIFR5 |= B00101111;// clear possible pending interrupts
}

void CalculateEngineSpeed()
{
  static unsigned int LastTime = 0;
  unsigned int ThisTime = TCNT5;
  
  // ThisTime < 1000 protects against the rare circumstance where Timer5 rollover occurs right after it is assigned (above); i.e. when ThisTime == 65535
  if (TIFR5 & B00000001 && ThisTime < 1000) { // handle TOV5 interrupt that is pending (occurs after this interrupt/function combo begins
    TIFR5 = B00000001; // clear TOV5 interrupts fired while this interrupt/function combo is being serviced
    CamPeriodT5RolloverCount++;
  }
  int Period = highByte(ThisTime) - highByte(LastTime) + (CamPeriodT5RolloverCount << 8);

  // this search-loop more efficient than calculating "newEngineHz = 1 / TimePeriod"
  byte newEngineHz = EngineHz;
  for (int i = 0; i <= MAXHZ; i++) { // setup a finite for loop to avoid getting logic-trapped
    if (Period > HzPeriodTable[0]) {  // stopped engine
      newEngineHz = 0;
      break;
    }
    else if (Period <= HzPeriodTable[newEngineHz] && Period > HzPeriodTable[newEngineHz+1]) break; // match found
    else if (Period > HzPeriodTable[newEngineHz]) newEngineHz--;  // engine has slowed down
    else if (Period < HzPeriodTable[newEngineHz]) newEngineHz++;  // engine has sped up
  }

  EngineHz = newEngineHz;  // accept the new EngineHz
  LastTime = ThisTime;
  CamPeriodT5RolloverCount = 0;
}

Note the function doesn’t explicitly calculate the engine speed in Hz. Instead it searches a pre-calculated array of highbyte(Timer5) tick counts for a matching range. The index values of this array represent the corresponding engine Hz. When a match is found the index value is stored as the current engine speed.

This search algorithm is faster than performing a division calculation on the processor, especially given that the engine speed changes gradually.  With this restriction the last known engine speed provides an excellent first-guess for the new calculated engine speed.  The search loop should only need to execute once or twice before arriving at a match.

There is additional code executed when the Timer5 rollover interrupt is fired.  This code increments the variable “CamPeriodT5RolloverCount”.  It’s value is greater than 1 at slow engine speeds when one full engine cycle takes more than 65,536 Timer5 ticks.

ATMega 1280 Capability 2010/03/22

Posted by Michael in 2JZduino.
2 comments

Before beginning this 2JZduino project it was necessary to determine whether the ATMega 1280 processor used in Arduino Mega was capable of the task at hand; managing fuel injector scaling and ignition delay for the 2JZ-GE Toyota engine.

Most important to the project is the ability to provide sufficiently high resolution in digital signal processing for the ignition timing signals.  Typically, tuning ignition of a performance engine is done on the order of 1 degree of engine rotation.  So, I deemed it necessary that the microprocessor platform must respond to an input event in less than 1/2 a degree of engine rotation at maximum engine RPM.

The 2JZ-GE engine redline is below 7000RPM (117 Hz).  Taking this as worst case, the period of rotation at this speed is 8.57 ms.  The engine rotates 1/2 of a degree in 11.90 microseconds.

The ATMega 1280 processor has a system clock speed of 16 MHz.  In 11.90 microseconds 190 instructions are executed.

So, the requirement for the firmware is that each event must respond and return in less than 190 system clock ticks; plenty of time for simple addition and toggling digital inputs and outputs.  However no multiplication, division, floating point math, or other relatively slow operations will be permitted within the interrupt routines.

It is worth considering the overall processor load due to event response.  For each combustion cycle of the engine there are 2 revolutions, 68 crank pulses, 6 cam pulses, and 6 injector pulses.  Altogether this totals 160 ON and OFF events.  At 7000 RPM this occurs over 17.14 ms (8.57 * 2) during which time there are 274,240 instructions executed.  If each event consumes the full 190 instructions allotted, this totals 30,400 instructions due to digital events.  30,400 / 274,240 = 11% processor load.

This 11% load is an important consideration.  The interrupt handling of the ATMega 1280 is fairly primitive; the highest priority interrupt is always serviced first, and the priorities are not configurable.  If interrupts arrive faster than they can be handled a low-priority interrupt might never be serviced.  The fact that the interrupt load is on the order of 10% suggests that interrupts are unlikely to overlap significantly.

From a practical standpoint digital signals arriving at 2JZduino from the engine and/or ECU are unlikely to ever overlap more than 3 events; cam, crank, and one injector.  Crank signals are spaced mechanically by about 5 degrees, and injector signals are spaced by the mechanics of each separate piston.  In the event that a cam, crank, and injector event do arrive simultaneously the worst-case scenario is that the most critical event is serviced last and as a result is delayed by a full 2 event service periods or, one degree of engine rotation.  If the last serviced event is that which the ECU uses for ignition timing the result is harmless delayed ignition (a fail-safe condition).

Ultimately, the ATMega 1280 does seem a capable processor… but care and efficiency will be necessary in designing and verifying the firmware.

Fuel Injector Drivers 2010/03/22

Posted by Michael in 2JZduino.
4 comments

[Edited April 10th 2010: Initially I had installed FQP4N20L MOSFETs. These generated too much heat (on resistance of ~1.2 Ohms). I have since switched to IRL640 which has an on resistance of only ~0.15 Ohms.]

The stock 2JZ-GE fuel injectors are 12V 12.5 Ohm solenoids.  There are 6 injectors total.  When the ECU requests fuel for each cylinder it switches the respective fuel injector wire to ground through a MOSFET of it’s own (I opened the ECU box to have a look at the PCB. It was indeed a MOSFET, although I didn’t record the actual part number).

2JZduino intercepts these fuel injector signals and recreates them through a custom designed Fuel Injector driver circuitry.  Below is a schematic of the fuel injector driver circuit for one of the 6 injectors. (Note the schematic shows IRF640, but IRL640 is the correct p/n.)


The stock injector ground path through the 2JZGE engine ECU (2JZGE ECU pin 10) is connected to one of the 2JZduino digital I/O pins (D21).  This pin is configured with the pull-up resistor ON; the firmware logic treats itas active LOW.  When the stock ECU requests fuel the 2JZduino firmware decides when to drive pin D35 output; this pin is active HIGH and controls the IRL640 MOSFET.

The Avalanche Diode (P6KE150 ) is used to protect against kickback from the solenoid.  After the solenoid has been ON, the instant the MOSFET gate is turned OFF (low) the energy in the solenoid coil (inductor) will be driven into the gate of the MOSFET as a voltage spike.  The Avalanche diode protects against this over-voltage by breaking down at 150V.  The selected breakdown voltage is a compromise between minimizing potential for damage to electrical components and maximizing the reverse voltage to ensure short “off time” of the fuel injector (the time required for the fuel injector to close).

Note that the IRL640 has a built-in diode that breaks down at 200V.  Relying on this internal protection however puts excess burden on the MOSFET to regularly dissipate energy (heat) every time the injector closes (every other engine revolution).  Instead, the dedicated diode takes on this responsibility.

The last two components are the 10k pull-down resistor connected between gate and ground, and the 220 Ohm resistor at the Arduino output.  The 220 Ohm limits the current rush out of the Arduino into the MOSFET gate.  The 10k pull-down ensures safe operation in the event that the 2JZduino loses power and the digital output connected to the MOSFET gate floats.  This resistor pulls the gate low so that the MOSFET is kept OFF unless explicitly driven HIGH by the digital output of the 2JZduino.

There are some necessary considerations for the power source of this circuit.  At 12.5 Ohms each of the 6 injectors will draw about 1 Amp when energized.  It is important to draw 12V power from a circuit that can supply the current.  Using the IS300 Overall Wiring schematics I found pin A5 which is part of the 25A EFI circuit.

This circuit has so far successfully driven all 6 fuel injectors on my 2JZ-GE engine during idle and up to ~3000 RPM (unloaded).  Additional testing is necessary to ensure the MOSFET heatsinks and thermal dissipation of the electrical enclosure provides adequate cooling.

VVTi Signal 2010/03/17

Posted by Michael in 2JZduino.
2 comments

One of my readers asked that I datalog the VVTi signal in a similar way to the Cam and Crank Signals.

Apparently VVTi operates at the alternator voltage: ~14V.  I built a 1:3 voltage divider using three 10k resistors to step the voltage down enough to be read by the Arduino analog input. I understand the VVTi signal is PWM.  Below is a chart of the captured signal – positive waveform only.

Download VVTi Signal Datalog.pdf for a full 3 seconds of raw data captured at ~8000 Hz.

Cam & Crank Signal Outputs 2010/03/17

Posted by Michael in 2JZduino.
3 comments

Through trial, error, and some help over at the Arduino Forums I discovered that the IS300 ECU most likely uses a Schmitt trigger to sense the input from the cam/crank variable reluctance sensors.

In a recent post I profiled the positive waveform of the cam and crank signals using Arduino (the negative waveform is presumed an approximate mirror of the positive).

Neither the IS300 ECU nor the Arduino Mega provide a negative DC source voltage, so generating a negative signal for the (assumed) Schmitt trigger posed a design challenge.  After some thought, I arrived at the circuit design shown below for the Cam sensor.  The Crank sensor circuit is identical.

The input-signal circuit and firmware logic (to be detailed in a future post) processes the incoming sensor signal and decides what state to put the output in: ON or OFF, for the simulated signal.

On the output side 2JZduino drives the IS300 ECU Cam Input directly.  The 47 mH inductor is used to generate a negative voltage spike at the Cam Input when the 2JZduino digital Out is turned OFF.  At that instant the energized inductor carries the Cam Input low while it depletes its magnetic field.  Note that the 82 ohm resistor is the real measured DC resistance of the inductor; Bourns RL622-473K.

Before selecting the 47 mH inductor and the 220 Ohm resistor I simulated the response using a first order approximation in MS Excel for the differential equation of an inductor:  v(t) = L di(t)/dt.  The graph below shows for the Crank Signal, the calculated ECU input voltage and the Inductor current for select engine RPM.  Note that the Crank signal fires for every 10 degrees of rotation.

The inductance and resistance values were chosen as a balance of inductor decay time, peak inductor current, and component availability.  With the components chosen the peak current through the inductor is 17 mA, and the negative waveform decay is reasonably long (ensuring the Schmitt trigger receives a sufficient negative signal).  This circuit has so far proven successful during idle and engine rev testing to ~5000 RPM.

Oxygen Sensors 2010/03/14

Posted by Michael in 2JZduino.
2 comments

Oxygen sensors in automotive applications are used for closed-loop fuel control to ensure the right amount of fuel is injected into the engine for combustion to match the incoming air metered at the intake tube.  For each type of fuel there is an optimal ratio between air (oxygen) and fuel to ensure complete combustion; the stoichiometric ratio.  For petrol this ratio is 14.7:1 by mass.  Too little fuel results in incomplete combustion and too much fuel results in unburned reactants.  Both cause pollution.

There are two types of commonly available oxygen sensors for use in automotive applications: wideband and narrowband sensors.  Narrowband sensors have a nonlinear response.  The signal saturates very quickly when the mixture is either rich or lean.  When the mixture is at the stoichiometric rate the signal is maintained at the midpoint of the output range.

Wideband sensors have a linear response to air/fuel ratio of a gas mixture.  This provides an opportunity to measure by how much a mixture is rich or lean.

The Lexus IS300 is equipped stock with two narrowband oxygen sensors for use in closed-loop fuel control.  I will be replacing both of these sensors with wideband oxygen sensors and connecting them to the 2JZduino.  2JZduino will then provide a programmable narrowband output signal for the stock ECU based on measurements of the wideband sensors.

Below is an IS300 narrowband oxygen sensor signal captured using a PC Soundcard-based Oscilloscope while the engine was at idle.

For more information on oxygen sensors see Wikipedia’s entry: http://en.wikipedia.org/wiki/Oxygen_sensor

2JZ-GE Cam & Crank Signals 2010/03/14

Posted by Michael in 2JZduino.
16 comments

The IS300 Overall Wiring diagrams show a camshaft and crankshaft position sensor connected to the ECU.  These sensors are variable_reluctance sensors and provide the ECU with engine position information, which is used for fuel and ignition control.

In preparation for building the electrical circuit and writing the firmware for 2JZduino, an investigation was necessary to identify the nature of these cam and crank sensor signals.  This would have been easiest with an Oscilloscope, but unfortunately I do not have easy access to one of these.  Instead, I built a data acquisition unit using the analog input channels of the Arduino Mega.  I tapped into the cam and crank signals at the ECU, connecting them to analog input channels 6 and 7.  A diode was connected between analog input signal (cathode) and ground (anode) to protect the Arduino against negative voltages generated by the sensors.

An excerpt of the code written to sample analog channels 6 & 7 is posted below.  Data acquisition rate for both channels is ~5,000 Hz, which is limited primarily by the serial port (limiting the speed at which data is extracted from Arduino).  The myDataLogger object (code not shown) has both “Store” and “Transmit” methods.  “Store” places the analog input values into a private member myDataLogger.buffer from within the interrupt service routine.  “Transmit” is called during the main loop of program execution which transmits the data sequentially over serial port to a listening software program which then re-assembles the data for graphing.

void setup()
{
  // setup ADC
  ADMUX = B01100110;  // AVCC VRef, ADC Left, ch6
  ADCSRB = B00000000; // Analog Input bank 1
  ADCSRA = B11001111; // ADC enable, ADC start
    // manual, interrupt enable, prescaler = 128
}

ISR(ADC_vect) { // Analog-&gt;Digital Conversion Complete
  static byte Channel6 = 0;
  static byte Channel7 = 0;

  char ADCch = ADMUX &amp; B00000111;  // which channel?

  if (ADCch == 6) {
    Channel6 = ADCH;  // store ADC result
    ADMUX = (ADMUX &amp; B11100000) + 7;  // set channel 7
  }
  else if (ADCch == 7) {
    Channel7 = ADCH;  // store ADC result
    ADMUX = (ADMUX &amp; B11100000) + 6;  // set channel 6
    myDataLogger.Store(Channel6, Channel7);
  }

  ADCSRA |= B11000000;  // start next ADC
}

The charts below show the captured cam and crank signals for my 2JZ-GE during engine cranking and during idle.  Note that these are the positive side of the waveform only because the Arduino analog inputs measure only positive voltages.

The engine idle is about 1300 RPM (this slows to ~700 RPM after the engine is warm).  Analysis reveals that the crank signal arrives for every 10 degrees of engine rotation and is generated 34 times in 360 degrees (there is no crank signal for 20 degrees of the revolution).  The cam signal arrives every 240 degrees of rotation so that there are 3 signals for each 720 degrees of rotation (each complete Otto cycle).  It seems that the stock ECU would identify the beginning of the engine cycle when the falling-edge of the cam signal and the 20 degree gap in the crank signal align.

Also worth noting is the magnitude of the peak signal voltage between startup (< 2 V) and idle (> 4 V).  The low peak-voltage at slow engine speeds will prevent signal detection to be done directly by the Arduino digital inputs.  This will be discussed in more detail when the circuit and acquisition logic design is discussed.

Engine Control for Forced Induction 2010/03/09

Posted by Michael in 2JZduino.
add a comment

I have intentions of adding a Raptor V supercharger to my IS300, mostly for the learning experience.  It is important to me that the supercharged engine be reliable and clean like the stock Lexus IS300 engine (the 2JZ-GE) while outputting about 350 hp (crank).

Increasing power output of an internal combustion engine is a simple matter of increasing the quantity of air that passes through it.  Turbochargers and superchargers increase power output by compressing air at the intake which increases the density of the ingested air and thus the mass flow-rate.

High combustion chamber pressures however require special consideration for ignition-timing, and fuel mixture to avoid detonation (engine knock or ping) and to optimize engine performance.  There are a few commercially available fuel/ignition control units that work in combination with stock ECUs, however I decided to design my own.

2JZduino is my custom signal processor for fuel and timing control.  Built on the open-source Arduino Mega platform, the solution includes a custom electronics board/shield that interfaces with the Arduino Mega, and the firmware to drive it.  All development is done specifically for the Toyota 2JZ-GE 3.0L inline-6 engine.

I post at the Arduino Forums under the username “Mitch_CA”.  In this space I will document the complete design.

Clutch Dampener 2010/03/09

Posted by Michael in my IS300.
add a comment

In the stock clutch hydraulic line, there is a Clutch Dampener Device (CDD).  This device acts as a flow restrictor when releasing the clutch pedal (engaging the clutch) but offers unrestricted flow when pressing the clutch pedal.

The design intent of this is presumably to soften the engagement of the clutch and contribute to some sort of Lexus luxury.  It is not effective for someone who prefers to feel more connected to the mechanics of the car.  Eliminating the effect is simple; disassemble the CDD and remove the flow restrictor components.  Below is a picture of the CDD disassembled.

The CDD is located on the driver side near the firewall.  Just follow the clutch hydraulic line from the reservoir.