Are 2 Arduino Uno's suitable to make a car ECU?

Hello guys!
Recently i had this idea on my mind that i should make a car ECU with Arduino Uno. What i wanted to achieve was to be able to control exact ignition timing and also to be able to control exact injector open time. Lets say i got a marking on a flywheel, which is sensed by a Hall sensor. It gives me the exact position and rpm of the crankshaft, the problem is that precision of the ignition i want to achieve is like 1/10th of a degree. By doing simple math we can tell how fast i need arduino to be which is ( 7000revs/min(max engine speed) * 360deg(flywheel is a wheel and wheel has 360degrees) ) / 60sec(minute has 60 seconds) * 10(because i want the resolution 1/10th of a degree) which is 420000 1/10th of degree every second. And i thinks thats like too much for arudino to handle. Basically what i want to do, is for example: after Hall sensor senses the point on flywheel - wait the time that flywheel needs to rotate 172.5degrees, and at that exact time produce a spark at the sparkplug. Second thing is after Hall sensor senses the point on flywheel - wait the time that flywheel needs to rotate 160degrees then open injector for exactly 0.8milisecond and close it. The question is - can Arduino handle this kind of speeds(420000 complex operations per second)? I got 2 separate Uno's so like i can make 1 controll ignition and the other one control injection. Or maybe i am thinking about it in the wrong way.

Sorry for my super bad english, i hope you got what i meant.
And also thanks for help.

1 Like

7000RPM is 116.6667 revs/sec; x360 degrees/rev gives 42000 degrees/sec. This is 23.81uS/degree or 2.381uS/tenth-degree.

A hardware timer in an Uno can be clocked at 62.5 nanoseconds (16MHz) giving 38 counts per 1/10th degree.

Is this for a 4-stroke engine? If so, the plug only fires once every 2nd full revolution. Therefore to do a cycle-by-cycle update to the ignition timing you have 720-degrees of time or 17.14mS. That's a fair chunk of time to do a spark advance calculation.

But do you have a need to compute spark timing on every single cycle? Your sensor inputs -- IAR, CTS, TPS, MAF/load, MAP etc -- will be way slower than this, especially filtered terms. You could probably update the spark timing 10 times a second -- every 100mS -- and still have an engine that runs fine.

The real problem with using an Uno for this is a lack of hardware timers. If you have more than one plug to fire (assuming a multi-cylinder engine) and want to use SFI (sequential fuel injection) you're going to need more hardware timers and/or some external hardware to help out.

What's the application?

2 Likes

Blackfin:
7000RPM is 116.6667 revs/sec; x360 degrees/rev gives 42000 degrees/sec. This is 23.81uS/degree or 2.381uS/tenth-degree.

A hardware timer in an Uno can be clocked at 62.5 nanoseconds (16MHz) giving 38 counts per 1/10th degree.

Is this for a 4-stroke engine? If so, the plug only fires once every 2nd full revolution. Therefore to do a cycle-by-cycle update to the ignition timing you have 720-degrees of time or 17.14mS. That's a fair chunk of time to do a spark advance calculation.

But do you have a need to compute spark timing on every single cycle? Your sensor inputs -- IAR, CTS, TPS, MAF/load, MAP etc -- will be way slower than this, especially filtered terms. You could probably update the spark timing 10 times a second -- every 100mS -- and still have an engine that runs fine.

The real problem with using an Uno for this is a lack of hardware timers. If you have more than one plug to fire (assuming a multi-cylinder engine) and want to use SFI (sequential fuel injection) you're going to need more hardware timers and/or some external hardware to help out.

What's the application?

Well thanks for reply.
Firstly i'd like to put it into some old 2-stroke motorcycle engine - which i would convert from carb to injection, and then if that would work i'd put it into old 88' Audi 100 5cyl 4stroke 2.1L engine which has mechanical injection (i'd convert that into electrical inj too). Let's just forget about all other sensors but the Hall for now. I can calculate a lot of things by using second arduino, but the point is: if the first arduino gives information to the second one, that it needs to make a spark lets say at 255.5deg. will the second one be able to do it accuratly enough? I know i need to use interrupts and other stuff. I have also seen that internal arduino timer1 has resolution of 4micros and is that enough? I want one Arduino just to "focus" on delivering a spark in that sweet spot and the second one on calculating that sweet spot based on other factors(which lets just forget about right now). I tried to do similar thigs with only one arduino but it had gargantuic deviations of +/-20milisec(yes mili not micro). Thats why i came up with the idea of using 1 solely for delivering spark. Also what happens when the timer1 overflows? Will it missfire at that moment?

1 Like

Things like ignition timing/spark control are usually done with input captures and output compares (i.e. timer interrupts.)

Suppose your crank position sensor is mechanically positioned to give a rising edge when the crankshaft is at, say, 40-degrees BTDC. That edge is 'captured" by the IC interrupt; that logs the timer value corresponding to the rising edge. Suppose the timer at that instant is 0x0123.

Suppose from previous cycles you've determined that the time for one complete revolution is 12mS (5000rpm); this means the crank is turning at 30,000 degrees per second giving 33.33uS/degree. If you wanted the ignition coil to fire at, say, 10oBTDC then you know you need to wait 40-10 * 33.33uS or 1000 timer ticks; therefore, you would set the output compare to the time if the IC (0x0123) plus 0x03e7 (1000d) or 0x050A.

You would configure the OC to produce a "falling edge" (i.e. cut current to the coil) which will then generate the spark.

You can add correction terms; how long does the coil field take to collapse to generate the spark? Subtract that from the number of microseconds so you can start the falling edge just that much sooner so the spark occurs exactly when you want.

Done properly, overflows won't affect operation.

Suppose the IC occurred at count 0xFFF5; 0xFFF5 + 0x03E7 = a timer value of 0x03dc. When the timer ticks out of 0xffff it rolls over to 0x0000 and starts up again. You need special consideration when the time delay is greater than the period of 65536 counts but this isn't a showstopper.

Food for thought: General Motors used 2MHz 8bit MCUs (68HC11 variants) in the late 80s and early 90s P4 ECMs to control their engines. These did everything: reading sensors, fuel and spark control, EGR, fans, idle speed control etc etc using these slow, memory-limited MCUs along with a smattering of external ICs.

I don't think you'd need two 16MHz 328Ps to control spark on an engine.

What are your planned inputs? What are your equations? Can you post your code attempt that didn't turn out so well?

Done right you can do an awful lot in a "lowly" Uno in the time it takes an engine to do its mechanical things...

2 Likes

Also there are many Arduino variants that have a lot more power than the UNO. So it makes more sense to use one of those, than to use two UNO's if you need more processing power.

2 Likes

Thanks for reply.
So basically right now i don't have any setup nor code. But lets say i want to "prepare" for real engine. Here im gonna show you what i want to do.
So basically lets say arduino 1 will emulate our Hall sensor - giving a pulse every now and then. How frequently those pulses are sent is controlled by potentiometer(Min value lets say 1000pulses per min, max 7000(so like map(raw, 0, 1023, 1000, 7000) where raw is analogread from a0 pin)). On the lcd i want 3 values - calculated rpm value, how much time it takes to do 1 full rev, and also after what time from interrupt it fires led(lets say basic value is like 170 deg after pulse). Lets assume that our sensor sends pulse when piston is at bottom dead center (crankshaft postion 0deg). I can also add potentiometer to arduino 2 to emulate some kind of TPS so we can change actual value of after what time from reciving interrupt it fires spark (id say -5deg for max analogread). Is that possible to do? If yes then how do i do that? im using <LiquidCrystal_I2C.h> library to manage my lcd. Im a little confused on how to use timings and iterrupts. Please help.

1 Like

Why reinvent the wheel? It's already been done with Speeduino

1 Like

detown:
Why reinvent the wheel? It's already been done with Speeduino

Home | Speeduino - Open, easy engine management

First i got Arduino Uno not Mega, second - i wanna learn its fun to do things that work - not just copy paste someone else's work.

1 Like

You could probably look at there code, which is open source and get an idea of how they implement some of there functions, such as how to decode a toothed trigger wheel and fire an ignition module.

1 Like

detown:
You could probably look at there code, which is open source and get an idea of how they implement some of there functions, such as how to decode a toothed trigger wheel and fire an ignition module.

Unfortunately this code is so advanced to me that it looks like ancient magic and i cant understand half of it.

1 Like

Such timings and the application of interrupts and so on is not a big mystery. It's just complicated (or maybe I should say detailed). It's all very well documented in many places. I suggest that you spend some time researching it, because there is no way that a useful and comprehensive tutorial on this subject can be presented in an internet forum thread. So you should apply your, "i wanna learn" to that stuff.

Also, actually getting a setup, and writing some code (any code) will help you progress immensely.

Here's a relatively simple example that uses Timer1 for input capture (CKP in) and for generating the coil control output.

It's intended to give a maximum dwell time of 4mS. Timing can be set from 10oATDC (max retard) to 20oBTDC (max advance.) I setup a 50% duty output from a Mega to drive it. Here are some waveforms (channel 0 is the CKP input, rising edge active; channel 3 is the ignitor (coil driver) output.)

In this image, we're at maximum RPM and maximum spark advance. (RPM is about 7000, advance is 20oBTDC). Notice that the dwell period starts at the CKP rising edge.

Minimum RPM (1000) and full advance. Notice that the 4ms dwell period starts much later:

Maximum RPM, (7000) and full retard (10ATDC):

Min RPM, full retard:

High RPM where it might be necessary to turn on the coil prior to the CKP edge to get the 4mS dwell time would require some more complexity in the code so I simply made it turn on at the CKP edge if the CKP period is short enough.

/*
 * Sketch:  spark_control.ino
 * Target:  Uno R3
 */
#define DWELL_TICKS         1000                //#     mS=N*4uS    4mS coil dwell time max
#define TPS_SAMPLE_TIME     50ul                //mS                TPS sample interval (50mS)

#define CKP_IRQ             0x01                //                  indicates to mainline that a CKP interrupt occurred
#define IC_TYPE_FLAG        0x02                //                  flag between IC and OC denotes stage of ignition cycle

//pins
const uint8_t   pinTrigger = 8;
const uint8_t   pinESC = 10;
const uint8_t   pinTPS = A0;
//
const float     BASE_ADV = 190.0;               //degrees ABDC (this is 10o ATDC, max retard)
const float     MAX_ADV_ADDER = 30.0;           //a fraction of this is subtracted from the BASE_ADV value to give net ADV

uint32_t
    timeNow_mS,
    timeTPS;
uint16_t
    rawTPS;

volatile uint8_t 
    vg_ICFlag = 0;
volatile uint16_t 
    vg_SpkTicks,
    vg_ulICPeriod;

void setup() 
{
    Serial.begin(115200);
    
    pinMode( pinTrigger, INPUT_PULLUP );
    pinMode( pinESC, OUTPUT );              //spark control output
    pinMode( pinTPS, INPUT );               //CKP sensor in
    //pinMode( 7, OUTPUT );                 //was used for debug

    uint16_t rawTPS = analogRead( pinTPS );
        
    //setup timer 1 input capture and output compares
    TCCR1A = _BV(COM1B1);                           //COM1B1 is set for both OCB modes we want
    TCCR1B = _BV(ICES1) | _BV(CS11) | _BV(CS10);    //input cap on rising edge (CKP sensor in) and prescaler=/64    
    TIFR1 |= _BV(ICF1);                             //clear any pending IC flags and enable the IC interrupt
    TIMSK1 = _BV(ICIE1);

}//setup

void loop() 
{
    //time to take a TPS sample?
    timeNow_mS = millis();
    if( (timeNow_mS - timeTPS) >= TPS_SAMPLE_TIME )
    {
        //save timestamp for next period and sample TPS
        timeTPS = timeNow_mS;
        rawTPS = analogRead( pinTPS );
        
    }//if

    //did we get a CKP edge?
    if( vg_ICFlag & CKP_IRQ )
    {       
        //yes; grab current value of the period and clear the CKP flag
        //use a "critical section" to read variables in case we get a CKP while reading
        //a multi-byte variable
        noInterrupts();
        uint16_t lcl_ulICPeriod = vg_ulICPeriod;
        vg_ICFlag &= ~CKP_IRQ;
        interrupts();
        
        //compute ticks from BDC for desired advance based on TPS              
        float fDegrees = BASE_ADV - (MAX_ADV_ADDER * (float)rawTPS / 1024.0);                
        uint16_t ticks = (uint16_t)(fDegrees * (float)lcl_ulICPeriod/360.0);
        
        noInterrupts();
        //make available to IC ISR
        //use critical section to ensure variable is properly updated
        vg_SpkTicks = ticks;
        interrupts();

        //display to serial monitor
        float rpm = 60.0 / (4.0e-06 * (float)lcl_ulICPeriod );        
        Serial.print( "\tRPM...: " ); Serial.print( rpm, 1 ); Serial.print( "\tAdv...: " ); Serial.println( 180.0 - fDegrees,1 );                 
                       
    }//if

}//loop

ISR( TIMER1_CAPT_vect )
{
    static uint32_t
        ulLastIC;
    static bool
        bFirst = false;

    uint16_t ulNowIC = ICR1;
    
    if( !bFirst )
    {
        //we need to use the first IC to grab the initial capture time
        //for use in computing the period
        bFirst = true;          //once we have the first, we're good
        ulLastIC = ulNowIC;     //save the current ICR1 as the last
        
    }//if
    else
    {                
        //compute the period of rotation (now minus last)
        vg_ulICPeriod = ulNowIC - ulLastIC;

        //if # of ticks before plug fires is < dwell time...
        if( vg_SpkTicks <= DWELL_TICKS )
        {
            //...set dwell to start right now, ~coincident with CKP
            TCCR1A |= _BV(COM1B0);      //set pin on OC
            TCCR1C |= _BV(FOC1B);       //force OC now (does not generate an interrupt)
            
            //dwell has started; now assign time of OC1B to generate spark edge
            OCR1B = ulNowIC + vg_SpkTicks;        
            TCCR1A &= ~_BV(COM1B0);
            vg_ICFlag |= IC_TYPE_FLAG;
             
        }//if
        else
        {
            //set coil to turn on 4mS before spark edge (dwell time)
            TCCR1A |= _BV(COM1B0);
            OCR1B = ulNowIC + vg_SpkTicks - DWELL_TICKS;
            vg_ICFlag &= ~IC_TYPE_FLAG;
            
        }//else
        
        vg_ICFlag |= CKP_IRQ;       //indicates to mainline that we got a CKP interrupt
        ulLastIC = ulNowIC;         //save this ICR as last for rotational period measurement
        
        TIFR1 |= _BV(OCF1B);        //clear any existing flags & enable the OC interrupt
        TIMSK1 |= _BV(OCIE1B);        
                        
    }//else
    
}//ISR - input capture

ISR( TIMER1_COMPB_vect )
{
    if( vg_ICFlag & IC_TYPE_FLAG )
    {
        //if flag is set, shut off the OC as we're done the cycle
        vg_ICFlag &= ~IC_TYPE_FLAG;
        TIMSK1 &= ~_BV(OCIE1B);
        
    }//if
    else
    {
        //flag clear; we're servicing the start of dwell
        TCCR1A &= ~_BV(COM1B0);         //set pin to fall next OC (will generate spark)
        OCR1B = OCR1B + DWELL_TICKS;    //set dwell time
        vg_ICFlag |= IC_TYPE_FLAG;      //set flag so next time in we end cycle
        
    }//else
    
}//ISR - output compare

Thanks everyone for help.

So after like 6hours of trying, 200pages of arduino pdf reading and another 2hr of youtube tutorials i think i managed to produce hall sensor emulator. Since i dont have any hall sensor i need to somehow emulate its behavior until i get my hands on one.

So on the top are 2 inputs - first is revolutions per second - second is for how big angle the hall sensor gives HIGH output(i dont know if in car it gives just a super short high pulse or it gives an angle but i assume it stays on for some time).

Anyway here is my code - i tried to make it as time sensitive as it was possible - thus not using any prescalers.

//revolutions per second of the engine(yes this is per second not per minute)[i concluded max rpm will be around 7000but i think it will work up to 10k]
double rps = 1;
//for how much degrees the hall sensor stays on (for example 22.5deg of an 360deg wheel[please dont insert more than 90{something is wrong with my maths or thinking}])
double degOfOn = 22.5;


//pins
const int coilPin = PB2;

//timer 1 value to compare
volatile uint16_t t1_comp;
volatile uint16_t t1_stopcomp;

//overflow counter and other counters
volatile uint16_t overFlow = 0; 
volatile uint16_t tickOver = 0;
volatile uint16_t tickStart = 0;

void setup() {
  Serial.begin(9600);//for debug
  
  DDRB |= (1 << coilPin); //Pin as an output.
  
  //Reset timer 1 register A and B
  TCCR1A = 0;
  TCCR1B = 0;
  //prescaler
  TCCR1B |= (0 << WGM12)|(0 << CS11)|(1 << CS10);
  //reset counter
  TCNT1 = 0;
  //enable timer 1 compare interrupt A B and overflow interrupt
  TIMSK1 |= (1 << OCIE1A) | (1 << OCIE1B) | (1 << TOIE1) ;
  //enable inters
  sei();
  
  //Calculate how many times it needs to overflow before it starts counting exact ticks to start pulse
  tickStart = (1/rps*16000000)/65536;
  //Calculate how many times it needs to overflow before it starts counting exact ticks to stop pulse
  tickOver =  (((1/((rps*360)/16000000))*degOfOn*2)+(1/rps*16000000))/65536;
  //calculate exact tick of pulse start after counter overflows tickStart times
  t1_comp = (1/rps*16000000)-(tickStart*65536);
  //calculate exact tick of pulse stop after counter overflows tickOver times
  t1_stopcomp = (1/rps*16000000)+((1/((rps*360)/16000000))*degOfOn*2)-(tickOver*65536);

  
  //set compares to compare variable
  OCR1A = t1_comp;
  OCR1B = t1_stopcomp;
}

void loop() {
  

  
  delay(1000);//simulate some very hard maths in loop here to check if it affects my interrupts, also slows down serial print
  
  Serial.println(tickStart, 10); //print how much times it overflows before it starts pulse
  Serial.println(t1_comp); //exact start tick
  Serial.println(tickOver, 10); //print how much times it overflows before it stops pulse
  Serial.println(t1_stopcomp); //exact stop tick
}

ISR(TIMER1_COMPA_vect) {//if start tick matches
  if(overFlow >= tickStart){//and also start overflow is good to go
    PORTB = (0 << coilPin); //start sending pulse
  }
}

ISR(TIMER1_COMPB_vect) {//if stop tick matches
  if(overFlow >= tickOver){//and also stop overflow is good to go
    PORTB = (1 << coilPin);//stop sending pulse
    overFlow = 0;//first reset overflow
    TCNT1 = 0;//then reset ticks
  }// and here we go again :)
}

ISR(TIMER1_OVF_vect) {
    overFlow++; //if it overflows just increment this counter
}

Any help on how to upgrade this code will be great. Also i don't really know how to make the whole 360deg to be filled with pulse.

Fueling is the hard part ...

Have a look a mega squirt and the methods and software for tuning it

The worst thing is that i dont have an oscilloscope and i cant measure output signals. :c

Thats why i wish someone could update my code to his arduino and check timings and other stuff because all the numbers are correct i suppose.

I ran this up for you and mostly it seems to be OK in terms of the outputs being steady, but the timing is off a bit. I changed the rps to 100 (ie 6000 RPM) and measure 11,240uS between falling edges. At 6000rpm every revolution takes 10,000uS so assuming you were aiming for 1 pulse per rev, your timing is off somewhere.

As a general recommendation, get rid of the floating point math in there. It will cause you nothing but problems for something like this and isn't needed at all.

If you're just looking at getting an Uno to output a signal to emulate a hall sensor, have a look at: GitHub - speeduino/Ardu-Stim

It does this out of the box for many different trigger patterns.