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