Coding my own aftermarket ECU [beta available]

Post Reply
User avatar
AngelMarc
Posts: 227
Joined: Sat Apr 08, 2023 9:23 pm
cars: A CB450 running to 8,000RPM with a P59.

Coding my own aftermarket ECU [beta available]

Post by AngelMarc »

EDIT: Most recent progress
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.

ECU.ino

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;
    }
  }
}
TuneArrays.h

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},
};

Original post-
Just going to dump progress and ideas of logic flow.
Updates will be sparse and slow. Largely from hunting for right syntax references.
I think spark timing/dwell will be handled by multiple arrays. One for start of dwell, one for end.
Same for fuel.
Will use 24x wheel. No cam, waste spark/batch fire.
Similar decode logic to what GM used.
AngelMarc wrote: Wed Apr 30, 2025 2:44 pm Ok, signed (+/-) start at zero, count down when low, up when high, move MSB of counter (for the +/-) each time signal goes low, into a shift register.
I knew it had to be a list of 1s and 0s, but I love this simple thing that can likely be done with common arduinos.
A little validation on a theory is nice every now and then. Great read.
With an array for the pattern to point at correct cylinder

Hopefully I don't run out of RAM.
If I can offset array values by a set amount for each cylinder, that'd be how timing is changed for V6, V8, V2 etc. As long as 15 degree increments work for the engine.
Sensor calibration will be done by the same array tables, deal with it.
Sensor count will be minimal.
No idle management routines.
Cylinder count limit currently unknown.
How to get it all going without overlap restrictions and racing-code conditions... I don't know yet.
Last edited by AngelMarc on Sun Jun 15, 2025 8:37 pm, edited 21 times in total.
Don't stress specific units.
User avatar
AngelMarc
Posts: 227
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 »

Nearly an hour just trying to get some formatting/syntax right so I can even see what space could be taken up by tune/cal.
It also didn't like me specifying "long" instead of "unsigned long"... "narrowing conversion from long long int, to long" ... um, no.
Attachments
Capture.PNG
Last edited by AngelMarc on Fri May 02, 2025 8:13 am, edited 1 time in total.
Don't stress specific units.
User avatar
pman92
Posts: 577
Joined: Thu May 03, 2012 10:50 pm
Location: Castlemaine, Vic
Contact:

Re: Coding my own aftermarket ECU

Post by pman92 »

You're missing a set of { } to cover the whole lot, and your array is backwards.
You've specified an array of 12 arrays with length 20. But then shown 20 arrays with length 12.

As for working out how much space, you dont actually need to define all the data. " = { }" is enough to allocate memory.
But even easier, 12 x 20 items of type unsigned long (4 bytes each) = 12 x 20 x 4 = 960 bytes of memory.
User avatar
AngelMarc
Posts: 227
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 »

Thank you. Need a quick reference manual as helpful as you. I'm not an encyclopedia that can remember all this arbitrary syntax.
Attachments
Capture.PNG
Don't stress specific units.
User avatar
pman92
Posts: 577
Joined: Thu May 03, 2012 10:50 pm
Location: Castlemaine, Vic
Contact:

Re: Coding my own aftermarket ECU

Post by pman92 »

Try chatGPT. It's excellent for stuff like that.
Just tell it you want short answers and you dont want it to try and take over.
If you let it start writing code for you, you'll generally get it working in the end, but you'll learn nothing in the process.
Just keep it for when you get stuck on stuff like that. The show it your code, show it the error you're getting, and it will point out the problem for you
User avatar
AngelMarc
Posts: 227
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 »

I forgot. My approach requires this insanity.
And compiler doesn't just tell me what that consumes.
Attachments
Capture.PNG
Capture.PNG (29.69 KiB) Viewed 1698 times
Don't stress specific units.
User avatar
AngelMarc
Posts: 227
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 »

EDIT: think I did my math wrong.
65,536 bytes, so before it was 131,072 bytes.
1 good thing about that circuit python nonsense, the units meant for it have excess resources.
Messed up the edit and deleted original. Don't know how I originally got 20kB.
Attachments
Capture.PNG
Capture.PNG (19.21 KiB) Viewed 1695 times
Last edited by AngelMarc on Wed May 28, 2025 2:03 am, edited 4 times in total.
Don't stress specific units.
User avatar
pman92
Posts: 577
Joined: Thu May 03, 2012 10:50 pm
Location: Castlemaine, Vic
Contact:

Re: Coding my own aftermarket ECU

Post by pman92 »

The [20][256] table is over 20kb by itself

20 x 256 x 4
User avatar
AngelMarc
Posts: 227
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 »

Oh, yes, I forgot to x4 that.
Don't stress specific units.
User avatar
AngelMarc
Posts: 227
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 »

To keep code more generic, I think I'll just compare 2 counters. Each counter will increment with the high and low of crank signal, then those numbers get compared with an if < or if > for the 24X decode.
Don't stress specific units.
Post Reply