Coding my own aftermarket 24x ECU [beta available]

MPC001
Posts: 43
Joined: Sat May 05, 2018 9:41 pm

Re: Coding my own aftermarket ECU

Post by MPC001 »

AngelMarc wrote: Sun Jun 15, 2025 4:14 am So, somehow I don't get to use standard base 10 numbers for the temp scaler arrays.
I mean, it makes enough sense, bit shifting vs base ten. 256 = no change.
Hard to say exactly how bad it's doing at 75,000 RPM
Capture.PNG
here's 39,000RPM with all the newest changes.
39.PNG
Not as good as before.
Might be a use for the extra tables after all. Though no doubt more niche.
Is that the crank waveform getting jittery as you add more code/load?
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU

Post by AngelMarc »

You mean the measured high/low time? Probably, I don't really have a good way to know how much.
Think the output was affected most by adding any interpolation and the predicted360 change.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU

Post by AngelMarc »

Crank waveform is coming from my signal generator running on an ESP32 S3, yellow line is cam sensor from ESP, red is injector output from Pi Pico ECU code, or spark depending on what I'm showing at the time.
The colors have been reversed in other video and pictures; but most recent is as described above.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU

Post by AngelMarc »

Added EOIT with it's own interpolation that get's disregarded when required to hit high injector duty cycle.
Double interpolation on primary tables (bilinear interpolation).
IAT scaling for spark and fuel.
Tables are 17x17 so anybody willing to tune manually can have a more reasonable time.
Current max RPM for tables is about 9,000. Start out more reasonable.
Still need to do a fair bit of sorting to get good starting numbers.
Only setup for a single cylinder, or even-fire twin.
At the risk of talking too soon, I think it's ready for some beta testing.

Code: Select all

#define CRANK_SENSOR_PIN 2  
#include "TuneArrays.h" 
//#include <mbed/platform/bare_metal/

enum PinState { WAITING_FOR_ON, WAITING_FOR_OFF };
PinState pinState = WAITING_FOR_ON;
unsigned long previousToggleTime = 0;
uint8_t RPMIndex = 0;
uint8_t adcIndex = 0;
bool pulseActive = false;
bool triggerPulse = false;
// Pin 11 (pattern1)
bool pulseActive11 = false;
bool triggerPulse11 = false;
unsigned long pulseStartTime11 = 0;
unsigned long currentOnDelay11 = 1000;
unsigned long currentOffDelay11 = 1000;
// Pin 4 (pattern0)
bool pulseActive4 = false;
bool triggerPulse4 = false;
unsigned long pulseStartTime4 = 0;
unsigned long currentOnDelay4 = 1000;
unsigned long currentOffDelay4 = 1000;

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

int readings2[24];       // Circular buffer
int bufIndex2 = 0;       // Buffer index
long total2 = 0L * 24;  // Set this to the initial sum

int readings3[24];       // Circular buffer
int bufIndex3 = 0;       // Buffer index
long total3 = 0L * 24;  // Set this to the initial sum

unsigned long lastTime = 0;
unsigned long pulseHighTime = 0;
unsigned long pulseLowTime = 0;
uint8_t signalHistory = 0;
bool newSample = false;
const uint8_t pattern0 = 0b00001111; //Fuel cycle strat/end point
const uint8_t pattern1 = 0b01111101; //Spark max advance point
uint8_t avg8bit = 0;

void handleCrankSignal() {
  unsigned long currentTime = micros();
  if (digitalRead(CRANK_SENSOR_PIN) == HIGH) {
    pulseHighTime = currentTime - lastTime;
  } else {
    pulseLowTime = currentTime - lastTime;
    uint8_t bit = (pulseHighTime > pulseLowTime) ? 0 : 1;
    signalHistory = ((signalHistory << 1) | bit) & 0xFF;
    if (signalHistory == pattern1) triggerPulse11 = true;
    if (signalHistory == pattern0) triggerPulse4 = true;
    newSample = true; // flag to loop()
  }
  lastTime = currentTime;
}

void setup() {
  analogReadResolution(12);
  pinMode(4, OUTPUT);
  pinMode(11, OUTPUT);
  digitalWrite(11, LOW);
  previousToggleTime = micros();
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(CRANK_SENSOR_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(CRANK_SENSOR_PIN), handleCrankSignal, CHANGE);
}

void loop() {
  if (newSample) {
    newSample = false;

    unsigned long RPMTime = pulseHighTime + pulseLowTime;//Calculate RPMtime (µs per 15 degrees)
    total2 -= readings2[bufIndex2];
    int newVal2 = RPMTime;
    readings2[bufIndex2] = newVal2;
    total2 += newVal2;
    bufIndex2 = (bufIndex2 + 1) % 24;
    int RPMTimeavg = total2 / 24;
    RPMTimeavg = constrain(RPMTimeavg, 278, 125000);  // 9000 RPM to 20 RPM

    // --- ADC rolling average (12-bit to 7-bit scale) ---
    total -= readings[bufIndex];
    int newVal = analogRead(A0);
    readings[bufIndex] = newVal;
    total += newVal;
    bufIndex = (bufIndex + 1) % 24;
    int avg12bit = total / 24;
    adcIndex = avg12bit >> 8; //Discarding LSBs to match array resolution
    uint32_t IndexFull_fixed = ((uint64_t)2500000 << 8) / ((uint32_t)RPMTimeavg * 600);
    uint Predicted360Time = RPMTimeavg * 24;
    uint16_t IndexWhole = IndexFull_fixed >> 8;
    uint8_t Frac = IndexFull_fixed & 0xFF;                                         
    int Frac2 = avg12bit & 0xFF;
                                                      
    int TerpA = Dstart[IndexWhole][adcIndex]; 
    int TerpB = Dstart[IndexWhole +1][adcIndex];                                   
    int TerpD = TerpA + (((TerpB - TerpA) * Frac) >> 8);
    int TerpA2 = Dstart[IndexWhole][adcIndex +1];                                           
    int TerpB2 = Dstart[IndexWhole +1][adcIndex +1];
    int TerpD2 = TerpA2 + (((TerpB2 - TerpA2) * Frac) >> 8);

    int TerpD5 = TerpD + (((TerpD2 - TerpD) * Frac2) >> 8);  

    int TerpA3 = Iend[IndexWhole][adcIndex+1];                                           
    int TerpB3 = Iend[IndexWhole +1][adcIndex +1];
    int TerpD3 = TerpA3 + (((TerpB3 - TerpA3) * Frac) >> 8);
    int TerpA4 = Iend[IndexWhole][adcIndex];
    int TerpB4 = Iend[IndexWhole +1][adcIndex];
    int TerpD4 = TerpA4 + (((TerpB4 - TerpA4) * Frac) >> 8);

    int TerpD6 = TerpD4 + (((TerpD3 - TerpD4) * Frac2) >> 8); 

    total3 -= readings3[bufIndex3];
    int newVal3 = analogRead(A1);
    readings3[bufIndex3] = newVal3;
    total3 += newVal3;
    bufIndex3 = (bufIndex3 + 1) % 24;
    int IATavg = total3 / 24;
    int IAT7bit = IATavg >> 5;
    
    int Temp = IATSpark [IAT7bit];
    int Scaled = ((TerpD5 * Temp) >>8);

    int TrimA = EOIT [IndexWhole];
    int TrimB = EOIT [IndexWhole +1];
    int TrimTerp = TrimA + (((TrimB - TrimA) * Frac) >> 8);

    int Temp2 = IATFuel [IAT7bit];
    int Scaled3 = ((TerpD6 * Temp2) >>8);
    int ScaledOffset = Predicted360Time - (Scaled3 + TrimTerp);
    if (ScaledOffset < 0) ScaledOffset = 0;


    // --- Update delay values from tables ---
    currentOnDelay11 = Scaled;
    currentOffDelay11 = 4000;

    currentOnDelay4 = ScaledOffset;
    currentOffDelay4 = Scaled3;
  }

  // --- Check for new pulse triggers from crank handler ---
  if (triggerPulse11) {
    triggerPulse11 = false;
    pulseStartTime11 = micros();
    pulseActive11 = true;
  }

  if (triggerPulse4) {
    triggerPulse4 = false;
    pulseStartTime4 = micros();
    pulseActive4 = true;
  }

  // --- Pulse timing for pin 11 (pattern1) ---
  if (pulseActive11) {
    unsigned long now = micros();
    unsigned long elapsed = now - pulseStartTime11;

    if (elapsed < currentOnDelay11) {
      digitalWrite(11, LOW);
    } else if (elapsed < currentOnDelay11 + currentOffDelay11) {
      digitalWrite(11, HIGH);
    } else {
      digitalWrite(11, LOW);
      pulseActive11 = false;
    }
  }

  // --- Pulse timing for pin 4 (pattern0) ---
  if (pulseActive4) {
    unsigned long now = micros();
    unsigned long elapsed = now - pulseStartTime4;

    if (elapsed < currentOnDelay4) {
      digitalWrite(4, LOW);
    } else if (elapsed < currentOnDelay4 + currentOffDelay4) {
      digitalWrite(4, HIGH);
    } else {
      digitalWrite(4, LOW);
      pulseActive4 = false;
    }
  }
}

Code: Select all

volatile int IATFuel[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};
//Comment above and uncomment below for no temp scaling, 256 = 100% or no change
//volatile int IATFuel[129] = {256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256};
volatile int IATSpark [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};
//Comment above and uncomment below for no temp scaling, 256 = 100% or no change
//volatile int IATSpark [129] = {256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256};

volatile unsigned long EOIT [17] = {500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500};

volatile unsigned long Dstart [17] [17] = { //lower numbers = more advance, numbers are in microseconds, colum lables manifold pressure, row lables RPM
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
{11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000, 12100, 12200, 12300, 12400, 12500, 12600, 12700, 12700, 12700},
};
volatile unsigned long Iend [17] [17] = { //injector on time, in microseconds, colum lables manifold pressure, row lables RPM
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},
};
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

I need to go add a rev limiter.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

added this to loop

Code: Select all

unsigned long Dwell = (RPMTimeavg < limiter) ? 0 : 4000; //rev limiter
this in TuneArrays.h

Code: Select all

volatile unsigned long Limiter = 277; //Spark cut rev limiter, microseconds per 15 degrees. divide 2,500,000 by this number to get RPM. Limiter is set after rolling average and before being constrained for table access, can be set beyond table limits
and altered this in loop

Code: Select all

currentOffDelay11 = Dwell;
Should do it. Not limited to RPM increments step sizes (600 RPM) and no interpolation.
Last edited by AngelMarc on Mon Jun 16, 2025 1:02 am, edited 2 times in total.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

That did it.
According to my chart, 277 is the number for 9,000 RPM
277.PNG
277.PNG (19.27 KiB) Viewed 271 times
And here are my oscilloscope measured ranges of spark and no spark
range.PNG
range.PNG (26.39 KiB) Viewed 271 times
Code updated in 1st post.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

Threw in an RPM minimum that cuts fuel and spark below 20RPM.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

Figured out how I'd add a cam sensor for full sequential.
An if before the list of pattern matches in the interrupt.
Then have an identical list of patterns, but change the "triggerPulse" number output.
Added instructions would just be to not have the edges of cam signal close to a pattern match.
50:50 or 1x cam signal.
Then I could swap predicted 360 for predicted 720 and really open up the dwell window, helping with higher RPMs.
Wouldn't be very fault tollerant.
Don't stress specific units.
User avatar
AngelMarc
Posts: 262
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Re: Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

For the hell of it, I tried compiling for a Teensy 4... it complains about syntax more, and comparing various variable types.
Annoying but a little interesting.
Don't stress specific units.
Post Reply