A simple beta is ready if anybody is willing to VE edit in the arduino source code tables.
https://www.youtube.com/watch?v=bqC3ijvPBOU 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.
Can be expanded to 4, 6, 8, or 12 cylinder fuel and spark later.
Current max RPM for tables is about 9,600. 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.
Added minimum of 20 RPM to shut off fuel and spark.
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
unsigned long total3 = 0UL * 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. +/- 15 degrees, round up to more advance
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];
unsigned long newVal2 = RPMTime;
readings2[bufIndex2] = newVal2;
total2 += newVal2;
bufIndex2 = (bufIndex2 + 1) % 24;
unsigned long RPMTimeavg = total2 / 24;
// FIXED: Rev limiter + low-RPM cutoff
unsigned long Dwell = 4000UL;
if (RPMTimeavg < Limiter) {
Dwell = 0UL; // spark cut when RPM too high
} else if (RPMTimeavg > 125000UL) {
Dwell = 0UL; // below 20 RPM, also cut spark
}
RPMTimeavg = constrain(RPMTimeavg, 261UL, 3000000UL); // 9600 RPM to 20 RPM. Always round the decimal up, even if it's 0.0001. Not overflowing arrays is important
// --- 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;
if (RPMTimeavg > 125000UL){
Scaled3 = 0UL; //Fuel off if RPM below minimum = 20RPM
}
// --- Update delay values from tables ---
currentOnDelay11 = Scaled;
currentOffDelay11 = Dwell;
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 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
volatile unsigned long EOIT [17] = {500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500};// these were test values, also in microseconds, change as needed
// 0 RPM -------------------------------------------------------------------------9600 RPM same 600 spacing
volatile unsigned long Dstart [17] [17] = { //lower numbers = more advance, numbers are in microseconds
{10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000},//"0" RPM
{9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500},// 600
{9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000},// 1200
{8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500, 8500},// 1800
{8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000},// 2400
{7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500, 7500},// 3000
{7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000, 7000},// 3600
{6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500, 6500},// 4200
{6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000},// 4800
{5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500, 5500},// 5400
{5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000},// 6000
{4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500, 4500},// 6600
{4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000},// 7200
{3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500, 3500},// 7800
{3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000},// 8400
{2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500, 2500},// 9000
{2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000} // 9600
};//low KPA --------------------------------------------------------------------------------- high KPA
volatile unsigned long Iend [17] [17] = { //injector on time, in microseconds
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},//"0" RPM
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 600
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 1200
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 1800
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 2400
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 3000
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 3600
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 4200
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 4800
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 5400
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 6000
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 6600
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 7200
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 7800
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 8400
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 9000
{1000, 2200, 3400, 4600, 5800, 7000, 8200, 9400, 10600, 11800, 13000, 14200, 15400, 16600, 17800, 19000, 20000},// 9600
};//low KPA --------------------------------------------------------------------------------- high KPA
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.
With an array for the pattern to point at correct cylinderAngelMarc 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.
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.