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;