12v square wave to 60-2 5v square wave BMW crank trigger simulator

Good afternoon, All. I'm looking to use an Arduino to convert a 12v square wave to a 5v square wave. I have a project car that I've swapped the engine and installed an aftermarket ECU on and I need to convert its tachometer signal to a signal that the cars original ECU can understand. A lot of the cars functions require knowing the engine RPM, dashboard, heat, air conditioning, etc. My aftermarket ECU outputs a 12v square wave at one pulse per revolution. I'm looking to convert this single 12v pulse to the 60-2 pulse 5v square wave. Meaning each 12v pulse would output 58 5v pulses followed by 2 empty spaces. I've done a bit of looking around and as of right now my plan is to use an optocoupler to drop the 12v to 5v for the Arduino to read the incoming signal and essentially run an array base function generator to convert the output.

Does this sound like it would work? Which Arduino, if any, would be best suited for something like this? Is there an easier or better way to accomplish something like this?

I'm a ME by trade so feel free to explain things like I'm a 5 year old. TIA

Using an optocoupler is good, a lot better than voltage divider.
Somehow You need to measure the frequency of the 12 volt pulses in order to calculate the timing to output 58 pulses + 2 "spaces".

I’ve been looking into how to read the incoming pulse length and create a variable to drive the timing for the array. It looks like I may be able to use “pulseIn” Thoughts?

Ehhh. Are You sure that the 12 volt pulses use a constant frequency and the length of the pulses carries the information?

There are some projects out there to simulate sensors ( inc missing tooth crank wheels) into ECU’s using arduino. - google will find them, also see
“ Ardu sim” project

Here's something that might get you started. It's very crude and not thought-out. If you scope the output you'll see the two-missing-teeth pulse drift in phase with respect to the incoming test pulses. I didn't devote any time to figuring out why.

The general idea here is to take the period (in timer ticks) between incoming pulses and divide that by 120. Then program an output compare to set/clear the OC1 pin until the last two OCs don't drive the pin (i.e. it stays low, giving the two-missing-teeth.) The period ticks divided by 120 is recalculated with each incoming RPM pulse. Therefore, the output pulse train is basically always one crank rotation behind the actual crank rotation. I don't think this would matter.

As to why the phase drifts, I don't know. Might think about it if I have some spare cycles later...

Note I use pin 7 to generate a test input pulse. Use a jumper to connect to pin 8. Pin 9 is the RPM output signal.

/*
 * Sketch:  bmw_60-2.ino
 * Target:  Uno
 */
#define MAX_RPM_PERIOD      0x3a98

const uint8_t pinRPMIn = 8;     //PB0 ICP1
const uint8_t pinRPMOut = 9;    //PB1 OC1A
const uint8_t pinTest = 7;      //test signal output (feed to pin 8 for test)

/*
 * Assume an RPM range from 250 to 7000RPM
 *  Input pulses:
 *      250RPM == 4.167r/s == 240mS/rev
 *      7000RPM == 116.667r/s == 8.571mS/rev
 *      Try /256 prescaler
 */

uint32_t
    timeNow;
volatile uint32_t
    timeoutLastPulse,
    timeoutCheck;
volatile uint16_t
    ticksOC1A;
volatile uint8_t
    pulseCnt;    
volatile bool
    bGotPulse = false,
    bFirstRPMEdge = true;

void setup( void ) 
{
    Serial.begin(115200);
    
    pinMode( pinRPMIn, INPUT_PULLUP );
    pinMode( pinRPMOut, OUTPUT );
    digitalWrite( pinRPMOut, LOW );
    pinMode( pinTest, OUTPUT );    
    
    //set up timer 1
    TCCR1A = 0;
    TCCR1B = _BV(CS12);     // /256 prescaler
    TIMSK1 = _BV(ICIE1);    //enable input capture

    pulseCnt = 120;
    timeoutLastPulse = millis();
    
}//setup

void loop( void ) 
{
    static uint32_t
        timeTest;
        
    CheckRPMTimeout();

    //generate very crude test output on pin 7
    timeNow = millis();
    if( timeNow - timeTest >= 100ul )
    {        
        digitalWrite( pinTest, HIGH );
        timeTest = timeNow;
        digitalWrite( pinTest, LOW );        
        
    }//if
    
}//loop

void CheckRPMTimeout( void )
{
    timeNow = millis();
    
    if( !bGotPulse && bFirstRPMEdge == false )
    {
        if( (timeNow - timeoutLastPulse) >= 240ul )
        {            
            timeoutLastPulse = timeNow;
            
            //timed out waiting for a pulse
            //reset pulse-out logic
            noInterrupts();            
            bFirstRPMEdge = true;       //need a new first edge
            TCCR1A = 0;                 //disconnect OC from PB1
            TIMSK1 &= ~_BV( OCIE1A );   //disable OC interrupts
            interrupts();            
            
        }//if
        
    }//if
    else if( bGotPulse )
    {
        timeoutLastPulse = timeNow;
        noInterrupts();
        bGotPulse = false;      //clear flag
        interrupts();
        
    }//else
    
    
}//CheckRPMTimeout

ISR( TIMER1_CAPT_vect )
{    
    static uint16_t
        timeLastEdge;
    uint16_t
        timeNowEdge = ICR1;
        
    if( bFirstRPMEdge )
    {
        bFirstRPMEdge = false;
        timeLastEdge = timeNowEdge;
        
    }//if
    else
    {
        bGotPulse = true;
        
        //find time between two rising edges
        uint16_t timePeriod = timeNowEdge - timeLastEdge;
        timeLastEdge = timeNowEdge;
        
        if( timePeriod > MAX_RPM_PERIOD )
        {
            bFirstRPMEdge = true;
            TCCR1A = 0;
            TIMSK1 &= ~_BV( OCIE1A );    
            
        }//if
        else
        {
            ticksOC1A = timePeriod / 120;            
            if( TCCR1A == 0 )
            {
                pulseCnt = 120;
                TCCR1A = _BV(COM1A1) | _BV(COM1A0); //set up to set pin on next OC                
                OCR1A = timeNowEdge + ticksOC1A;
                TIMSK1 |= _BV( OCIE1A );    

            }//if
            
        }//else
        
    }//else
    
}//input capture

ISR( TIMER1_COMPA_vect )
{
    OCR1A = OCR1A + ticksOC1A;    
    
    if( pulseCnt > 5 )
    {         
        //if in pulsing region, 
        if( TCCR1A == (_BV(COM1A1) | _BV(COM1A0)) )   //if setting pin now
            TCCR1A = _BV(COM1A1);   //config to clear it next compare
        else
            TCCR1A = (_BV(COM1A1) | _BV(COM1A0)); //otherwise, config to set pin next compare
            
    }//if
    else if( pulseCnt > 1 )
    {
        //if we're in region of "missing pulses" config pin to stay low
        TCCR1A = _BV(COM1A1);   
        
    }//else if
    else
    {
        //done this rotation; reset for next
        pulseCnt = 120;
        TCCR1A = (_BV(COM1A1) | _BV(COM1A0)); //otherwise, config to set pin next compare
        
    }//else

    pulseCnt--;   
        
}//output compare

@Railroader, according to the documentation for my Aftermarket ECU the 12v square wave that outputs the RPM signal is 50% duty cycle and 1 pulse per rotation. I think that I would be able to time the length of that pulse and output my 60-2 5v pattern on the same time scale?

@Hammy, looking into Ardu-Stim. Looks like its a bit overkill for what I'm trying to do but will definitely have some valuable info.

Thanks guys!

@Blackfin, amazing! It's gonna take me a week to understand all that but I'll get there. Looks like that'll do exactly what I need. I'm trying to keep this as small as possible, will something like a pro mini or a trinket MO work? Thanks!

fattymcchubbs:
@Railroader, according to the documentation for my Aftermarket ECU the 12v square wave that outputs the RPM signal is 50% duty cycle and 1 pulse per rotation.

The RPM information is in the frequency! 1 pulse per revolution.

I think that I would be able to time the length of that pulse

Not interesting.

Measure the time period, time per one revolution. That's the time frame You've got to launch the pulses needed. Then calculate the RPM.

Okey. If the PWM s exactly 50% measuring the pulse, multiplicating it by 2 You get the time frame.

The fundamental problem is that in order to produce the 58 timed pulses, you need to know the duration of the entire revolution. Which you don't know until it's over ... and then it's too late! I think Blackfin's approach is to simply use the duration of the preceding rotation as a substitute, which sounds reasonable provided the rpms don't change too quickly.

The assumption is that you need to have the 58 pulses spaced evenly throughout the entire revolution. If you don't need that, then it's a lot easier. :slight_smile:

S.

My application won't have an issue if there's a slight, sub 250ms, delay on the output of the 60-2 signal. It's not actually being used to control anything more so to keep the BMW ECU happy thinking that the engine is running. In all honesty it would still function if I were to just trick it into thinking the engine was always running at 1000 RPM... but I do want the tachometer to function which is why I'm looking to do things this way. My reason for wanting to us an array to drive my signal is that it should scale the pulses. Much like some of the variable sine wave generators I've seen on here.

fattymcchubbs:
@Blackfin, amazing! It's gonna take me a week to understand all that but I'll get there. Looks like that'll do exactly what I need. I'm trying to keep this as small as possible, will something like a pro mini or a trinket MO work? Thanks!

A 5V Pro Mini should be okay.

A Trinket M0? Not so sure; I've not used it. My code example above uses the timer hardware for the AVR (so think ATmega328P, ATmega2560...) You'd have to see how the timer functions in the SAMD21E18, whether the necessary pins are brought out to GPIOs (there's only 5 available on the Trinket from what I can tell...) and how to use them.

In that case your job just got much easier.
Measure the number of millis from one rising edge of one 12v pulse to the next -- call that "A"
Divide A by 116 (or 120), call the result "B"
Have the Arduino change state of a digital pin every B millis.
Like Blackfin said, you're always one rev behind, but my interpretation of what you've written is that it shouldn't matter.

If the output pulses from the ECU are reasonably clean, all you need is a voltage divider to feed an Arduino input.
S.

If any of the functions require knowing the engine position you might be better off piggybacking the crank sensor. Not too familiar with bmw but if it's a VR sensor you should be fine, if it's hall or optical you may have to disable the pullup in your aftermarket ecu.

It would be interesting to see if a simple software solution would be sufficient for your purposes.

const byte InputPin = 4;
const byte OutputPin = 5;


unsigned long MicrosecondsPerHalfOutputCycle = 1000;
uint8_t OutputHalfCycleCount;  // Count up to 120


void setup()
{
  pinMode(InputPin, INPUT);
  pinMode(OutputPin, OUTPUT);
}


void loop()
{
  static unsigned long halfCycleStartTime = 0;


  // Output 60-2 square wave
  if (micros() - halfCycleStartTime >= MicrosecondsPerHalfOutputCycle)
  {
    halfCycleStartTime += MicrosecondsPerHalfOutputCycle;
    OutputHalfCycleCount++;
    if (OutputHalfCycleCount >= 120)
      OutputHalfCycleCount = 0;


    // Skip half cycles 0 and 2 for the two missing teeth
    if (OutputHalfCycleCount > 3)
      digitalWrite(OutputPin, (OutputHalfCycleCount & 1) == 0);  // High on even half-pulses
  }


  // Measure microseconds per revolution.
  static int oldInputState = LOW;
  int inputState = digitalRead(InputPin);
  if (inputState == HIGH && oldInputState == LOW) // Rising edge
  {
    static unsigned long oldRisingEdgeTime = 0;
    unsigned long risingEdgeTime = micros();
    unsigned long inputRevolutionMicroseconds = risingEdgeTime - oldRisingEdgeTime;
    oldRisingEdgeTime = risingEdgeTime;


    // Now calculate microsecond per output half-cycle
    MicrosecondsPerHalfOutputCycle = inputRevolutionMicroseconds / 120;  // 120 half cycles
  }
  oldInputState = inputState;
}

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.