jump to navigation

Volumetric Efficiency 2011/04/15

Posted by Michael in 2JZduino.
6 comments

Early on in developing 2JZduino I made an assumption that the Injector Pulse length for each combustion cycle was exclusively a function of Manifold Pressure. The reasoning was that independent of all other variables the volumetric efficiency of the air intake system would remain mostly constant and a certain Manifold Pressure correlates strongly to the volume of air in the combustion chamber.

This is grossly incorrect. In hindsight it’s obvious that the engine replaces air at varying efficiencies (which explains things like why there is such a strong torque curve in a 4-stroke engine), but I was mistaken in the magnitude of the effect. And being so large, the effect of VE has a significant impact on fuel trims (which were previously assuming VE ~= 90%), upwards of 400% under some engine conditions.

To go about resolving this I logged a fair amount of engine data intentionally exercising a range of engine speeds and manifold air pressures so that I could calculate volumetric efficiency in post processing. The datalogger object in the 2JZduino code measures Engine Hz, MAP, AFR (banks 1 & 2), and Injector Pulse Length every 167ms. With this information I have measurements for the amount of air in the combustion chamber (calculated from AFR and Injected Fuel amount), and quantity of air for 100% efficiency (manifold pressure). The actual volumetric efficiency for each data-point is then calculated as the ratio of the two. The calculation looks like this…

2JZduino datalogger Measurements:
Hz = instantaneous engine speed
MAP = instantaneous manifold air (absolute) pressure, kPa
AFR = Air Fuel Ratio (latent)
T_inj = injector pulse length for previous combustion cycle, ms

Constants and Knowns:
Q_inj = 3.32 g/s = fuel injector flow rate
Inj_Latency = 1 ms = opening time of the injector solenoid
V_cyl = 0.5L = cylinder volume (swept)
CR = 10.5 = engine compression ratio
d_Air = 1.25 g/L = approximate air density
P_atm = 101 kPa = atmospheric pressure

Solving for the combustion chamber air mass that corresponds to 100% volumetric efficiency:
V_chamber = 0.5 + 0.5/(10.5-1) = 0.553L = total volume of combustion chamber
M_air_100% = 0.553 * d_Air = 691mg = air mass for 100% VE and MAP = P_atm

Solving for the instantaneous air mass in the combustion chamber for the instantaneous measurements: e.g. for Hz = 38, MAP = 66 (absolute, T_inj = 5.0, AFR = 14.1
M_fuel = (T_inj – Inj_Latency) * Q_inj = 13.3 mg
M_air = M_fuel * AFR = 186.5 mg

Solving for measured VE:
VE = M_air / (M_air_100% * MAP/P_atm) = 186.5 / (691 * 66/101) = 41.3%

Using these calculations for every logged data point I was then able in post processing to begin separating/sorting/analyzing the relationship between engine speed, manifold pressure, and volumetric efficiency. I eventually arrived at the following formula empirically that calculates volumetric efficiency (percent) as a function of engine speed and manifold pressure…

Eq. #1)
VE(Hz, MAP) = 12 + (Hz * MAP)/320 + MAP/2.67 – Hz/16 + 100/Hz
…constrained as VE > 20% and VE > 90%

This relationship was then used to calculate and predict (rather than measure) the volumetric efficiency for each data point in the datalog, and then further predict the pulse length that would be required for the measured AFR. Finally the error is calculated between actual injector pulse length and predicted injector pulse length. Continuing from the example above the calculation is as follows…
VE(38, 66) = 44.8%
M’_fuel = (V_chamber * d_Air * MAP/P_atm)/AFR * VE(Hz, MAP) = 14.4 mg
T’_inj = Inj_Latency + M’_fuel/Q_inj = 5.3 ms
Pulse Length Error = 5.3 – 5.0 = 0.3 ms

As verification to the empirical VE formula, the graph below shows select portions of the data-logs that present the measured and calculated (using Eq. #1) injector pulse lengths alongside the error between them. For reference the corresponding engine MAP is also shown.

Clearly, the errors between calculated and measured injector pulse lengths are held mostly to less than 1ms. Exceptions to this occur primarily under conditions where injector pulse-lengths change rapidly (caused by rapid changes in throttle position). In these instances the response times of sensors and even the 2ZJduino are the likely sources of error.

This particular calculation for VE (Eq. #1) shows to be a rather good approximation. Now with an understood of volumetric efficiency for my particular 2JZ-GE, next steps are to apply the calculated VE values to the fuel trim map in 2JZduino. More on this in a future post.

For reference, below is a map of measured Volumetric Efficiency of my 2JZduino for Engine Hz vs. Manifold Air Pressure…

UnoEngineSim and (hacked) Interrupt Priorities on Arduino Mega 2011/04/05

Posted by Michael in 2JZduino.
add a comment

During testing I began to experience a misfire that was only occurring above about 5500 RPM. I wondered if the igniter signals were sometimes being delayed due to over-lapping interrupts that were higher priority. I’d recently purchased an Uno as a second Arduino and so I wrote an engine simulator that would run on the Uno to test my theory.

The Uno was configured to produce Injector and Igniter output signals at the intervals typical of a 2JZ-GE engine running at about 6000RPM. These outputs were connected to the inputs of 2JZduino. One-at-a-time the signals *output* by 2JZduino were connected back into the Uno on Pin8; the Input Capture pin, so that the latency of 2JZduino could be measured. The Uno would then report out statistics on the time from when the simulated output signal was generated, to the time that 2JZduino recreated the signal (captured by the ATmega 168’s Timer1 Input Capture register).

Below is a copy of the EngineSim code run on the Uno. Once a second, it reports out the # of events, minimum/maximum/average event delays, and the number of events that fell below the average (providing an estimate of the spread of the data).

// Pin assignments
// Pin2(PD2) = Inj1sim
// Pin3(PD3) = Inj2sim
// Pin4(PD4) = Inj3sim
// Pin5(PD5) = Inj4sim
// Pin6(PD6) = Inj5sim
// Pin7(PD7) = Inj6sim

// Pin8(PB0) = Input Capture

// Pin9(PB1) = IGT1sim
// Pin10(PB2) = IGT2sim
// Pin11(PB3) = IGT3sim

// ********************
// Establish all timing for engine speed of ~100Hz = 6000RPM (28us / deg)
// ********************

volatile unsigned int tEventQueue; // time the event was queued
volatile unsigned int tEventDelayMax; // max elapsed time from Event-queued to Input-Capture
volatile unsigned int tEventDelayMin; // min elapsed time from Event-queued to Input-Capture
volatile unsigned long tEventDelaySum;
volatile unsigned int tEventCount;
volatile unsigned int tEventCountSmall;
volatile unsigned int tEventDelayAverage;

void MonitorInput()
{
  TCCR1B |= B10000000; // Noise Canceler = ON, Capture Falling Edges
  TIMSK1 |= B00100000; // enable Input Capture interrupt
  DDRB &= B11111110; // B0 is an input
  PORTB |= B00000001; // B0 pull-up resistor ON
}

void StartIGTandInjSim()
{
  DDRB |= B00001110; // IGT1..3 sim on PB1..3
  PORTB &= B11110001; // Ensure IGT1..3 signals are OFF
  DDRD |= B11111100; // Inj1..6 sim on PD2..7
  PORTD |= B11111100; // turn on Inj1..6sim -> signal is active low
  TCCR1A = 0; // Normal operating mode
  TCCR1B = B00000010; // clk/8 -> 32.77ms roll-over
  TIMSK1 |= B00000110; // enable OCIE1B and OCIE1A 
}

void StopIGTandInjSim()
{
  PORTB &= B11110001; // Ensure IGT1..3 signals are OFF
  PORTD |= B11111100; // turn on Inj1..6sim -> signal is active low
  TIMSK1 = 0;
}

ISR(TIMER1_COMPA_vect) // IGT sim
{
  // Toggle Sequence: 1,1,2,2,3,3
  const byte IGTbitmaskSeq[6] = { B00000010, B00000010, B00000100, B00000100, B00001000, B00001000 };
  static byte IDX = 0;

  if (IDX == 1) tEventQueue = TCNT1; // Queue the event for IGT1 ON
  PINB = IGTbitmaskSeq[IDX]; // toggle in sequence
  IDX++;
  if (IDX >= 6) IDX = 0;
  
  OCR1A += 5247; // Next event: 3250/65536 * 32.77ms = 1.62ms -> 3 IGT pulses (equally spaced) each revolution
}

ISR(TIMER1_COMPB_vect) // INJ sim
{
  PIND = B11111100; // toggle all the injectors simultaneously
  OCR1B += 10003; // Next event: 10000/65536 * 32.77ms = 5ms -> Sim. all Injectors pulse each revolution
}

ISR(TIMER1_CAPT_vect)
{
  unsigned int Delay = ICR1 - tEventQueue;
  if (Delay > tEventDelayMax) tEventDelayMax = Delay; // store the peak delay that occurred
  if (Delay < tEventDelayMin) tEventDelayMin = Delay; // store minimum delay that occurs
  if (Delay < tEventDelayAverage) tEventCountSmall++;
  tEventDelaySum += Delay;
  tEventCount++;
}

void StartSim()
{
    StartIGTandInjSim();
    MonitorInput();
}

void StopSim()
{
  StopIGTandInjSim();
  tEventDelayMax = 0;
}

void setup() {
  Serial.begin(115200);
  Serial.println("IGT1 ON Event Delay in clk/8 counts...");
  MonitorInput();

  StartSim();
  Serial.println("Begin...");
}

void loop() {
  if (Serial.available())
  {
    char c = Serial.read();
    
    if (c == 's') {
      StartSim();
      Serial.println("Begin...");
    }
    else if (c == 'x') {
      StopSim();
      Serial.println("stop.");
    }
  }
  if (tEventDelayMax > 0) 
  {
    Serial.print(tEventDelayMin, DEC);
    Serial.print(" .. ");
    Serial.print(tEventDelayMax, DEC);
    Serial.print(" (");
    Serial.print(tEventDelayAverage, DEC);
    Serial.print(":");
    Serial.print(tEventCount, DEC);
    Serial.print(":");
    Serial.print(tEventCountSmall, DEC);
    Serial.println(")");
  }
  else Serial.print(".");
  tEventDelayAverage = tEventDelaySum/tEventCount;
  tEventDelayMax = 0;
  tEventDelayMin = 65535;
  tEventDelaySum = 0;
  tEventCount = 0;
  tEventCountSmall = 0;
  delay(1000);
}

Note that the Injector and IGT signals are intentionally setup on different frequencies so they drift in and out of phase in over-lap. This was done to ensure a worst-case scenario would eventually surface where the injector events, ADC_complete events, timer overflows, etc. would all interfere with the IGT events. What I found is that on some occasions the igniter signals would be delayed by upwards of 600us. At 5500 RPM this translates to about 20 degrees. The Uno simulator was written as worst-case (with all 6 injectors firing simultaneously), but it was at least seeming plausible that under some circumstances interrupt conflicts could cause the igniter signal to be delayed long enough that a misfire could occur because the engine spark arrived too late.

Looking at the datasheet for the Atmega 1280, the Interrupt vectors show that the IGT interrupt requests (Pin Change Interrupt Requests 0/1/2) are lower in priority than the Injector interrupt requests (External Interrupt Request 0/1/2/3/4/5). What I wanted was for the IGT interrupts to be treated with higher priority than the Injector interrupts. Spark events are critical in timing, but getting slightly more or less fuel would have a negligible effect. The solution I arrived at was to configure the interrupt handlers for the Injector External Interrupts like this…

ISR(INT0_vect, ISR_NOBLOCK) { // Ext Interrupt 0 on D0
  noInterrupts();
  myIS300.h_InjectorChange(0);
  interrupts();
};

In this way with the “ISR_NOBLOCK” argument, if all 6 Injector interrupts and an IGT interrupt occurred simultaneously the external interrupts would all queue up on the stack, each one interrupting the other, until the IGT interrupt gets serviced as the highest priority interrupt. The IGT interrupt, being a blocking interrupt, would be fully-serviced, and then the Injector interrupts would start getting popped off the stock one-by-one, unwinding until normal program flow continued.

With this change the maximum delay on the IGT signal was reduced from 600us down to 40us. 95% of the time the IGT signal delay was < 20us. This translates to better than 1deg of latency in the IGT signal at 6000 RPM under worst-case conditions.

It turned out that the real-problem causing the high-RPM misfire was due to a 220 Ohm resistor being installed where there should have been a 10k Ohm resistor. But the above was a good improvement nonetheless.

0.3 Alpha (3rd public release) 2011/04/05

Posted by Michael in 2JZduino.
12 comments

I’m over-due for another release. As mentioned in my post 0.2 Alpha (2nd public release). I’ve been working on replacing the crank sensor intercept with IGT signal intercept. This is now working successfully and is implemented in v0.3 available here…
2JZduino at SourceForge

Here’s a list of significant features new to v0.3…

  • Developer notes are now found in “DevNotes.txt” (they used to be at the top of “IS300_MAPadd.cpp”).
  • Note the inclusion of “IS300_Arduino_EEPROM.exe” in the sourceforge download. This program will talk to and program the EEPROM values into 2JZduino.
  • Igniter control – the code no longer supports interception and reproduction of the crank signal. Instead it intercepts and reproduces the igniter signals: IGT1, IGT2, IGT3. “IGT ON” events are always executed immediately (which charges the igniter circuit). “IGT OFF” events (which marks the firing of the spark) are executed depending on TimingRetardTable[][] values.
  • Fuel Injector pulse-adjustments are now calculated based on the Fuel Injector size stored in EEPROM.
  • Simulated narrowband circuit ground is now connected through PortB0 (Arduino pin 53). The code floats this pin as an input until the LC-1s show they are ready. As an input, the high-impedance drives the stock Toyota ECU into open-loop mode until the LC-1 begins outputting a meaningful signal. When it switches to an input (held low), the ground patch triggers the stock ECU to switch into closed-loop.
  • Improved interrupt efficiency. Igniter interrupts are now always serviced within 40us of their occurrence (20us 95% of the time). Injector interrupts are serviced within 60us of their occurrence.
  • Engine speed now calculated from IGTon signals instead of Crank sensor signals.
  • Datalogger expanded to now include RPM, MAP, Bank1 AFR, Bank2 AFR, and Injector pulse-width data. Datalogger information is now saved in a .csv file by “IS300_Arduino_EEPROM.exe” using the current date/time in the filename.
  • Reduced amount of SRAM used at run-time.
  • Added advanced injector and MAF compensation logic to EEPROM that will scale fuel injectors based on the new vs. stock fuel injector size, new vs. stock MAF sensor intake tube diameter, fuel injector lag, and a DC bias for the MAF signal. *Note: this is experimental and unverified. More details on this in a future post.

Additionally, here’s a summary of the verification testing that I’ve completed since the last release. I’ll be posting some more analytical information on these in the near-term, but for now, here are the results.

  • My 2nd LC-1 wideband has been installed. I’ve now gathered more than 20hours of operation with 2JZduino providing the simulated Narrowband signal for closed-loop fuel control. It’s been successfully fooling the stock ECU into adjusting for an AFR of 15.1 instead of 14.7 while under closed-loop operation.
  • ~4 hours of operation with 2JZduino providing active Injector Scaling (providing leaner fuel injection amounts at wide-open-throttle).
  • Verified that all 10k resistors are indeed 10k resistors… one of the resistors in the circuit for Injector #4 was found to actually be a 220 Ohm resistor. This culprit was understandably causing me all kinds misfire problems for awhile. I’ve learned a good lesson here :)