Open Source ECU

Im curious why you are making this for 60,000 RPM engine? I thought the most advanced engines in the world were only capable of around 20,000 RPM?

60,000RPM was an early test. As the loop code get’s more complex, it’s max RPM will diminish. If it maxed out at 20,000 at that point, my odds of success would be near zero without effectively starting over.

2 Likes

I was overthinking it.
image

… looking like Google Sheets doesn’t have a simple interpolate option.
Just trying to fill in sane starting numbers. Using an LM7 tune as a reference, because got to start somewhere.

To interpolate cheap, I’ll probably get my index number (0-31), save it, save it plus one(1-32), pull each of those VE/spark numbers, then use the decimal (from result of {(2,500,000 / (RPMtime * 600))}) as the percentage scaler between index and index+1.


Numbers pulled from stock LM7 tune.
image

Between 5,952 and 6,037.2 measured RPM I see some very messy and Inconsistent scaling. Like swinging between the 2 adjacent values and sometimes in the middle. More accurate to say the range of interpolation “noise” is Frac = 0 to Frac = what the code calculation says it should be. Not sure what to make of it yet.

    // --- Calculate RPM timing (µs per event) ---
    unsigned long RPMTime = pulseHighTime + pulseLowTime;
    RPMTime = constrain(RPMTime, 134, 125000);  // 18600 RPM to 20 RPM
    IndexFull = (2500000/(RPMTime*600));
    IndexWhole = (int)IndexFull;
    IndexPlusOne = IndexWhole + 1;
    TerpA = Iend[IndexWhole][adcIndex];
    TerpB = Iend[IndexPlusOne][adcIndex];
    Diff = TerpB - TerpA;
    Frac = IndexFull  - IndexWhole;
    TerpC = Diff * Frac;
    TerpD = TerpA + TerpC;


    // --- Update delay values from tables ---
    currentOnDelay11 = Dstart[RPMIndex][adcIndex];
    currentOffDelay11 = Dend[RPMIndex][adcIndex];

    currentOnDelay4 = Istart[RPMIndex][adcIndex];
    currentOffDelay4 = TerpD;

No interpolation, just involving a truncated float. Horrible results

    unsigned long RPMTime = pulseHighTime + pulseLowTime;
    RPMTime = constrain(RPMTime, 134, 125000);  // 18600 RPM to 20 RPM
    IndexFull = (2500000/(RPMTime*600));
    IndexWhole = (int)IndexFull;
    //IndexPlusOne = IndexWhole + 1;
    //IndexPlusOne = constrain(IndexPlusOne, 0, 31);
    //TerpA = Iend[IndexWhole][adcIndex];//
    //TerpB = Iend[IndexPlusOne][adcIndex];//
    //Diff = TerpB - TerpA;
    //Frac = .5; //IndexFull - IndexWhole;
    //TerpC = Diff * Frac;
    //TerpD = TerpA + TerpC;



    // --- Update delay values from tables ---
    currentOnDelay11 = Dstart[IndexWhole][adcIndex];
    currentOffDelay11 = Dend[IndexWhole][adcIndex];

    currentOnDelay4 = Istart[IndexWhole][adcIndex];
    currentOnDelay4 = Iend[IndexWhole][adcIndex];
    //currentOffDelay4 = TerpD;

Replacing

    IndexFull = (2500000/(RPMTime*600));
    IndexWhole = (int)IndexFull;

with

    IndexWhole = (int)(2500000/(RPMTime*600));

Get’s rid of the weird double spikes… somehow.

ChatGPT suggested fixed point code that works better, but still seems awefuly sloppy.

unsigned long RPMTime = pulseHighTime + pulseLowTime;
RPMTime = constrain(RPMTime, 134, 125000);  // Clamp RPMTime for your RPM range

// Calculate IndexFull in fixed-point 16.16 format
uint32_t IndexFull_fixed = (uint32_t)((2500000ULL << 16) / (RPMTime * 600UL));  // <<16 for fractional bits
uint16_t Frac_fixed = IndexFull_fixed & 0xFFFF;      // Fractional part (0 to 65535)
uint8_t IndexWhole = (IndexFull_fixed >> 16);        // Whole part

IndexWhole = constrain(IndexWhole, 0, 31);
uint8_t IndexPlusOne = constrain(IndexWhole + 1, 0, 31);

// Get lookup values from your table
uint16_t TerpA = Iend[IndexWhole][adcIndex];
uint16_t TerpB = Iend[IndexPlusOne][adcIndex];

// Integer linear interpolation: TerpD = TerpA + ( (TerpB - TerpA) * Frac_fixed ) >> 16;
int16_t Diff = (int16_t)TerpB - (int16_t)TerpA;
uint16_t TerpD = TerpA + ((int32_t)Diff * Frac_fixed >> 16);

// TerpD is your interpolated result, integer, .00 precision (whole number)

I sweep the analog input and see a range that looks just as bad as float.

A simple beta is ready if anybody is willing to VE edit in the arduino source code tables.
Currently only targeting Pi Pico. Timer strategy requires it.
If you have a different Arduino compatible 32bit board with a 64 bit timer and 2 analog inputs, feel free to try.
All code is generic for easy porting of the entire project or just snippets, like crank decode logic.
Maybe Alpha is more appropriate.
DM if interested/require instructions.
GM 24x crank signal only.
17x17 tables using bilinear interpolation.
Only setup for a single cylinder, or even-fire twin, or single cylinder 2 stroke.
Current max RPM for tables is about 9,000. Start out more reasonable.
Whatever BAR MAP sensor you want.
IAT scaling for spark and fuel.
Added EOIT that get’s disregarded when required to hit high injector duty cycle and has it’s own interpolation.
If you want, you can turn off temp scaling pretty easy, or leave it on just for fuel and use a potentiometer as a manual choke.
Waste spark only, injector is same, once per revolution. I plan to test on a small 2 stroke first.
There’s 50 microseconds of “noise”/inaccuracy on output pulse timing, and there seems to be a minimum on time before it just stays zero, I think that number was 100-150 microseconds.
Added a spark-cut rev limiter.

Source code here Coding my own aftermarket ECU [beta available] - pcmhacking.net

1 Like

So the idea is to use a Pi Pico to make any ECU work with something like a mini gas generator?

The idea is for it to be an ECU for whatever the hell you want.
As long as you’re willing to mount a custom crank trigger wheel.
Right now it’s only set up for single cylinder. I’ll change that when I care to.

1 Like

You should add logic with an option trigger for glow plugs (humble request) a 2-stroke compression ignition should only need an initial warmup procedure, especially if it’s analog carb.

So if I’m getting this right, you’re making the pi pico the ECU? No intermediary ECU crunching data?

I’m imagining this software unfurling in to a beautiful open source sandbox that can run a hydrogen + waste plastic oil fuel mixture in a DIY 2 stroke cylinder with maybe even a plugin system for emissions controls

Have you thought of posting on codeberg?

Correct, just the Pi Pico. And it only supports the GM 24x crank signal.
The external input would support emissions related real time adjustments.
There’s no target AFR, just open loop and it pulses based on tune, so whatever fuel you want to set it up for. I don’t know much about glow plugs, but I won’t be trying to add that… push a button. If a fuel pump output with it’s primer pulse would work, I might make it easy to just have the prime pulse and point you where to change it to not keep that output on while running.
“codeberg”? Never heard of it.

Made account and put some links up.

1 Like

Lol, when you put it that way. Just curious because yeah you could do a timed relay for the glow plugs with a push button but integrating glow plug logic could allow for engine-on duty cycles to maintain engine temp in colder environments. But could also be achieved with an intake air heater

If you just want the glow plug control based on temp, tell AI you want arduino code to create a pulse width that varies based on an array of 17 values and I could point out the relevant analog sensor code that points to the array and does the interpolation that effectively increases the resolution by a lot with minimal accuracy drift.

globals

volatile unsigned long glow[129] = {128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,256,258,260,262,264,266,268,270,272,274,276,278,280,282,284,286,288,290,292,294,296,298,300,302,304,306,308,310,312,314,316,318,320,322,324,326,328,330,332,334,336,338,340,342,344,346,348,350,352,354,356,358,360,362,364,366,368,370,372,374,376,378,380,382,384};

int readings[24];       // Circular buffer
int bufIndex = 0;       // Buffer index
long total = 0L * 24;  // Set this to the initial sum

setup()

  analogReadResolution(12);
  pinMode(A0, INPUT);
  pinMode(4, OUTPUT);

loop()

    total -= readings[bufIndex];
    int newVal = analogRead(A0);
    readings[bufIndex] = newVal;
    total += newVal;
    bufIndex = (bufIndex + 1) % 24;
    int avg12bit = total / 24;
//Above is signal averaging. used as a noise filter.
    adcIndex = avg12bit >> 5; //Discarding LSBs to match array resolution
    int Frac2 = avg12bit & 0x1F;//Keeping only LSBs, fraction of the whole

    int TerpA = glow[adcIndex]; 
    int TerpB = glow[adcIndex +1];                                   
    int TerpD = TerpA + (((TerpB - TerpA) * Frac2) >> 5);//Fractional is used as a whole number, bit shifting negates that offeset.


   digitalWrite(4, HIGH);
   delayMicroseconds(TerpD);
   digitalWrite(4, LOW);
   delayMicroseconds(200);

That should do it or at least get you pretty close.
I got lazy at the end with the delays. My code has all it’s loop in an ‘if’ that gets triggered by the crank sensor interrupt. This is just, turn it on and it does a thing autonomously.
Using effectively different analog resolutions in different parts of my ECU had me thinking I found an error. Had to rewrite a little of that as I pasted it in.

1 Like

That’s fixed point math by the way.
Some of it is 8 bit decimal, some is 5 bit.