jump to navigation

Crank Signal Input Detection Logic 2010/07/02

Posted by Michael in 2JZduino.

Accurately reproducing or delaying the crank signal relies on accurate detection of the incoming crank signal. Through testing I’ve determined the signals arriving from the crank VR sensor range from as little as ~0.8V outputs peak (during engine start) up to +30v peak at high RPMs.

Unfortunately at low speeds the signal peak is not sufficiently high to trigger the Arduino digital inputs to TRUE. And so the input signal is connected to both a digital input and analog input. The mode that it operates in is dependent on the engine speed. At slower engine speeds the signal is detected using the ADC that is on-board Arduino. At slow speeds the crank signals arrive slowly enough that the ADC can capture them adequate resolution. At higher engine speeds the logic relies on digital input interrupts to accurately capture the crank signal.

The input circuit for the VR sensor consists of a Schottky diode (1N5817) and Zener diode (1N4733A) both connected between signal (cathode) and ground (anode). The Schottky diode provides protection for the Arduino inputs when the VR sensor goes below -0.5V, and the Zener diode provides protection for the Arduino inputs when the VR signal rises above 5.1 V. A schematic of the input circuit is shown below.

The crank input signal is connected to pin 49: Timer4 Input Capture Pin. For digital input mode this provides a method to capture the precise Timer4 value in the Input Capture Register (ICR) when the event occurred (which may not be the same time that the interrupt gets serviced).

Timer4 input capture events can be either rising or falling edge. In either case, the response is similar. 2JZduino either reproduces the signal immediately, or stuffs it in a to be reproduced later. The decision is based on the Manifold Air Pressure at the time of the event. The amount of delay is stored in EEPROM as a 25-element byte-array. Each byte stores the delay in 10ths of a degree for Manifold Air Pressures ranging from 80-180KPa (in 4KPa increments). At startup this data is converted into a two-dimensional lookup table (30×25) of unsigned integers. This pre-calculated table stores the delay in # of timer counts for a 2-dimensional lookup of Manifold Air Pressures and Engine speed measured in Hz. The conversion is done upfront to prevent expensive math operations during processing of the input signals.

The calculation between 10ths of a degree and timer counts is shown here…

  for (int i=0; i < RPMIntervals_LookupTable; i++) {  // calculate the Timing Retard table
    for (int j=0; j < BoostPressureIntervals_LookupTable; j++) {
      TimingRetardTable[i][j] = (unsigned int)((556/HzInterval) * EEPROM.read(TimingRetard_EEPROMadr + j) / (i +1) );

Constants RPMIntervals_LookupTable = 30, BoostPressureIntervals_LookupTable = 25, and HzInterval = 4. Note that Timer4 is configured to run at 1/8th of the system clock (2MHz). The # of timer ticks for timing delay is calculated as…
Timer_ticks = 2*10^6 [ticks/second] / 360 [deg/rev] * Delay [deg] / EngineHz [seconds/rev]

Since Delay is stored in EEPROM in 10ths of degrees the first 3 terms simplify to the constant 556 (2*10^6 / 360 / 10). Also, EngineHz is stored in the table in buckets of 4Hz so the value of i is multiplied by HzInterval (=4). The result is the calculation shown above which has been arranged to guarantee that there is never an overflow in any of the calculations.

With that explained, here is the interrupt handler for a Crank signal event…

ISR(TIMER4_CAPT_vect)  // use Timer4 InputCapture interrupt with Noise Cancellation for CRANK signal detection
  unsigned int InterruptTime = ICR4;
  if (TCCR4B & B01000000) { // Rising Edge events caught by Input Capture
    if (CrankFilter_Abort(true, InterruptTime)) return;  // don't process this Rising Edge if the CrankFilter rejects it
    TCCR4B &= B10111111;  // switch to falling edge detection
    if (TCNT4 - InterruptTime + MIN_OCR_OFFSET > TimingRetardTable[EngineHzIndex][MAPindex]) h_ExecuteCRANKON_Event();  // execute immediately if it is already/almost late
    else if (TIMSK4 & B00000010) { // OCR4A is already scheduled
      if (!CrankONQueueFull) { // use the CrankONQueue if it's available
        CrankONQueueFull = true;
        CrankONQueueTime = InterruptTime + TimingRetardTable[EngineHzIndex][MAPindex];
      else {
        GenerateErrorMessage("Crank ON collision!   ");  // check for OCR4A over-run
        CrankONQueueFull = false;  // reset the queue
    else ScheduleOCR4A(InterruptTime + TimingRetardTable[EngineHzIndex][MAPindex]);
  else {
    if (CrankFilter_Abort(false, InterruptTime)) return;  // don't process this Falling Edge if the CrankFilter rejects it
    TCCR4B |= B01000000;  // switch to rising edge detection
    if (TCNT4 - InterruptTime + MIN_OCR_OFFSET > TimingRetardTable[EngineHzIndex][MAPindex]) h_ExecuteCRANKOFF_Event();  // execute immediately if it is already/almost late
    else if (TIMSK4 & B00000100) { // OCR4B is already scheduled
      if (!CrankOFFQueueFull) { // use the CrankOFFQueue if it's available
        CrankOFFQueueFull = true;
        CrankOFFQueueTime = InterruptTime + TimingRetardTable[EngineHzIndex][MAPindex];
      else {
        GenerateErrorMessage("Crank OFF collision!   ");  // check for OCR4B over-run
        CrankOFFQueueFull = false;  // reset the queue
    else ScheduleOCR4B(InterruptTime + TimingRetardTable[EngineHzIndex][MAPindex]);

First, the input signal must receive a value of TRUE from the CrankFilter_Abort function. This function is a noise filter. It stores the last rising->falling pulse length. If less than 1/4 of this pulse length has elapsed since the previous acceptable pulse, then the current pulse is rejected. The code is shown below…

boolean CrankFilter_Abort(boolean IsRising, unsigned int EventTime)
  static unsigned int T_LastPulse = 0;
  static unsigned int T_LastEdge = 0;
  if ((EventTime - T_LastEdge) < (T_LastPulse >> 2)) return true; // abort if this signal is too near the last one
  if (!IsRising) T_LastPulse = EventTime - T_LastEdge;
  T_LastEdge = EventTime;
  return false;  // this signal is ok to process

Next, the signal delay is evaluated against MIN_OCR_OFFSET. If the current time (TCNT4) is almost the same as the delayed signal event time then it gets executed immediately. This prevents accidentally scheduling an event for the past (which would manifests itself as a long delay, executing finally after Timer4 rolls-over and wraps around).

The last option is for the event to be scheduled for future execution. This either gets scheduled directly in the Output Compare Register, or queued if the Output Compare Register is already in use (which occurs when the timing is delayed by more than the distance between teeth on the crank reluctor wheel). Event scheduling and signal reproduction logic (h_ExecuteCRANKON_Event) will be covered in a future post. If there is no room to schedule this event a Collision Error is reported to the LCD.

For Analog operating mode the crank sensor signal is read repeatedly by the ADC. Here, the logic looks for the Crank signal level to be either above or below a threshold, and changes the output level if required. The ON threshold varies between 30 (~0.6V) when the Engine is stopped, and 60 (~1.2V) when the engine is at normal idle speed. The actual value depends on an average of recent Peak Crank signal values. No timing delay logic is performed when in the analog operating mode.

    byte OFFthreshold = 10;
    byte ONthreshold = 30;
    if (EngineHz <= ENGINEHZ_MINIDLE) ONthreshold = 30;  // reset threshold to 30 at slow engine speeds (for startup)
    else if (ONthreshold < 60) ONthreshold = min(60, PeakCrankADC);  // ramp threshold up to 60 once it starts

    Crank_ADCvalue = ADCH;  // store the Left Adjusted ADC High Byte value
    if (Crank_ADCvalue > peakCranklocal) peakCranklocal = Crank_ADCvalue;
    if (!(*CamCrankOutputPinReg & CrankOutputBitMask) && Crank_ADCvalue >= ONthreshold) {
      if (!CrankFilter_Abort(true, TCNT4)) h_ExecuteCRANKON_Event();  // process this Rising Edge only if the CrankFilter accepts it
    else if ((*CamCrankOutputPinReg & CrankOutputBitMask) && Crank_ADCvalue <= OFFthreshold) {
      if (!CrankFilter_Abort(false, TCNT4)) { // process this Falling Edge only if the CrankFilter accepts it

        static byte counter = 0;
        static unsigned int PeakSum = 0;
        PeakSum += peakCranklocal;
        peakCranklocal = 0;
        if (counter == 64) { // PeakCrankADC value is an average of 64 readings
          PeakCrankADC = byte(PeakSum >> 6);  // divide by 64
          PeakSum = 0;
          counter = 0;


1. Nik - 2010/11/04

Excellent work as always Michael, I am learning a lot from your posts :) I have one query. I see you had a lot of issues dealing with the output from the variable reluctance sensors and ended up using a combination of analogue and digital inputs to get around this. I am curious though, if your aim is just to delay the ignition then would it not be simpler to allow the stock ECU to do handle the signal conditioning and then just capture the output (pin IGT on the stock ECU from memory) which is a conditioned square wave signal used to trigger the ignition event. You could ‘catch’ this with your digital input and then insert a delay of your choosing and then create another square wave which would go to the igniter. Do you think this would work or have I missed an important step?

2. Michael - 2010/11/04

Nik, this is so obvious it’s brilliant. Very early I assumed the signal out available at the ECU would be something I could not connect to the input side of an Arduino, and so went down the path of intercepting the VR sensor output.

What do you know about the IGT output signal? Is it 5V peak? How much current does the igniter sink?

Another consideration that’s coming to mind is whether this might lead to P03xx engine misfire codes… if for e.g. the ECU expects ignition 10deg earlier than it actually occurs will it eventually detect a misfire?

I will give this some further thought. Thanks for the idea.

3. Nik - 2010/11/06

I’m glad my suggestion turned out to be useful :) I’m a bit new to arduino (I dont even have one yet), but I know my share about toyota engine management. The IGT signal would be 5v peak and I don’t believe the igniter takes a lot of current, however it would be worth putting some protection in there for the arduino input.
With regard to the engine misfire codes. The ECU expects another signal called IGF which signifies that an ignition event was successful. The code is triggered if the ECU misses several of these in succession (I think it needs to miss about 6 or more of them). So a 10 degree delay will not be an issue. In the worst case scenario though if there was an issue you could always fool the stock ECU by making up your own IGF signal with one of the arduino outputs.
I have a project of my own in mind to upgrade the 2JZ ignition system using an arduino to interface with some aftermarket ignition components. I’ll be ordering a couple of boards tonight. I’ll also need to do some signal conditioning of my own on the NE and G signals so I’m thinking of using an LM1815 schmitt trigger IC on them. I’ll put a scope on it to have a look at the signals while I’m at it. I’ll also put the scope on the IGT and IGF wires so that I can let you know how the signals actually look in reality and if they are consistent with expectations

Michael - 2010/11/08

So I’ve been giving this some more thought. I do like the idea of intercepting IGT… it would reduce the workload on the ECU, and would also provide a much better means to intervene in the event of reading excessive pressure in the intake; by cutting fuel and spark (previously I’d planned to delay-ignition and maybe add fuel in the case of too much intake pressure).

From an Arduino standpoint three more inputs attached to hardware interrupts are required to detect IGT 1/2/3 immediately, which *should* be available (I need to check my schematic vs. Mega pinouts). The crank signal input would still be required to properly measure engine speed (to calculate IGT delay), but it wouldn’t need to be measured at low RPM (where no ignition delay is required) and it could be setup to read falling edge only instead of rising and falling (further simplifying things).

Let me know what you find when you scope the outputs. If no additional components are needed to protect Arduino from the IGT ECU signal this could be a very simple revision to implement.


4. Martin Johann Kloppers - 2012/02/09

Any further developments in this regard? I have an MR2 and am trying to get a decent signal into my arduino for MaxMSP to display/utilize RPM… I am currently running an MSD 6A ignition box, coil, and tach adapter, just in case there are additional options there.

Michael - 2012/02/12

Hi Martin. Yep, I’m now intercepting the IGT signals directly instead of the crank signal. Have a look at the code for specifics, but I first mentioned it in this post here…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: