YFA task loop code

updated 01 may 2023

Here's a 01 May 2023 snapshot of the main YFA calculation. This is fully functional but minimally featured version that stores single tables to EEPROM. The in-dev version uses a faster CPU for more timing accuracy and multiple table support using SD cards.

The fetchDatum()/sendMessage() system is part of inter-process-communication (IPC) and also a simple key:value database. Other tasks (eg. sensors) generate named datums.

/*

  Carter YFA "feedback carburetor" closed-loop and open-loop control.
 
  This provides automatic control of engine AFR via the
  YFA's enleanment solenoid, using an in-dash Autometer A/F meter's
  0..5V output, manifold pressure, rpm, and coolant temperature as inputs.

  Close-loop control is a through a "block learn" system taken directly
  from 1980's GM ECUs, with a short-term integrator and a 2D table of
  A/F corrections. 
  
  Instead of a VE table there is a 2D table of target AFR values. Since 
  the carburetor itself handles all fuel calculations the calc is mainly 
  carb behavior correction. Altitude compensation comes out in the wash.


  SPEED AND TIMING AND PWM


     +-------------------------+                       +----
     |                         |                       |
     |                         |                       |
     |                         |                       |
  ---+                         +-----------------------+

     |<------------- PWM PERIOD (100 mS) ------------->|

                                                | calc time

  The YFA's solenoid drive is spec'd by Carter/Weber in Document 36008B
  at 10Hz/100 mS PWM, where solenoid-on causes an air leak in the emulsion 
  circuit: enleanment. One percent, one millisecond, was chosen to be the 
  working resolution.

  https://www.ramblerlore.com/AMC/Feedback-YFA-revisit/Service_Carter_YF_YFA.pdf
  https://www.ramblerlore.com/AMC/Carter-YF/Service_Carter_YF_YFA.pdf

  The main loop runs the YFA state machine (YFAPWM) and times
  the calculation to be complete before the start of the next PWM cycle.
*/

/*
       6 AFR - Rich Burn Limit (engine fully warm)
       9 AFR - Black Smoke | Low Power
    11.5 AFR - Best Rich Torque at Wide Open Throttle (WOT)
    12.2 AFR - Safe Best Power at Wide Open Throttle (WOT)
    13.3 AFR - Lean Best Torque
    14.6 AFR - Stoichiometric Air/Fuel Ratio Value (Stoich)
    15.5 AFR - Lean Cruise
    16.5 AFR - Usual Best Economy
      18 AFR - Carbureted Lean Burn Limit
     22+ AFR - EEC / EFI Lean Burn Limit
*/

// These define working data ranges and the table dimensions.
// The table does not contain row/col for 0th row/col; no running
// engine will produce that data, so to-row and to-col calcs
// offset it.
//
// Table contents are integer packed-decimal values, xx.yy. TAFR
// values run 10.00..18.00, and ELM runs 0.00..99.99.
//
const int RPMBIN =      400;              // RPM bin spacing (columns) and lowest value
const int MAXRPM =     3600;              // eg. columns 400..3600
const int RPMBINS = MAXRPM / RPMBIN;

const int KPABIN =       10;              // KPa bin spacing (rows) and lowest value
const int MAXKPA=        99;
const int KPABINS = MAXKPA / KPABIN;

// Autometer A/F ratio panel meter, with 0..5V analog output that
// maps 0V=10.00 AFR, 4V=18.00 AFR. The meter does hang at ignition-on,
// and when it does, it reads 10.00. If outside the operable range
// we assume AFR meter not-ready or error.
//
const float MINAFR =     10.50;           // operable AFR range
const float MAXAFR =     18.00;

// A/F RATIO GOAL TABLE 
//
// Working table copied from on-road adjusted TAFR dump.
// Tables do not contain 0th column and rows. Each row/col label
// ("10 KPa") is the low value in that range, eg. 10..19.
//
static int16_t FIXEDTAFR [KPABINS] [RPMBINS] = {

// 20 Apr 2023.
/*  rpm        400    800   1200   1600   2000   2400   2800   3200   3600  */
/*  10 KPa */ 1250,  1300,  1500,  1600,  1600,  1600,  1600,  1600,  1600, 
/*  20 KPa */ 1300,  1300,  1600,  1600,  1600,  1600,  1600,  1600,  1600, 
/*  30 KPa */ 1300,  1300,  1400,  1560,  1600,  1600,  1600,  1550,  1500, 
/*  40 KPa */ 1300,  1300,  1350,  1580,  1600,  1600,  1600,  1550,  1500, 
/*  50 KPa */ 1300,  1300,  1350,  1500,  1550,  1550,  1550,  1500,  1500, 
/*  60 KPa */ 1300,  1300,  1350,  1500,  1500,  1500,  1500,  1500,  1450, 
/*  70 KPa */ 1300,  1300,  1350,  1480,  1480,  1480,  1480,  1480,  1400, 
/*  80 KPa */ 1300,  1350,  1350,  1350,  1350,  1350,  1350,  1350,  1350, 
/*  90 KPa */ 1300,  1280,  1280,  1280,  1280,  1280,  1280,  1280,  1250, 
/*  rpm        400    800   1200   1600   2000   2400   2800   3200   3600  */
};

// initial hand edited table.
// This is too rich all over, but does run.

///* KPa InHg */
///*  rpm       400   800  1200  1600  2000  2400  2800  3200   */
///*  10 27 */ 1200, 1200, 1200, 1250, 1500, 1600, 1650, 1650, 
///*  20 24 */ 1200, 1200, 1200, 1300, 1500, 1600, 1600, 1650, 
///*  30 21 */ 1200, 1200, 1200, 1300, 1400, 1500, 1500, 1550,
///*  40 18 */ 1200, 1200, 1250, 1300, 1350, 1500, 1550, 1650, 
///*  50 15 */ 1250, 1250, 1300, 1350, 1350, 1500, 1550, 1550, 
///*  60 12 */ 1250, 1250, 1320, 1350, 1450, 1450, 1500, 1500, 
///*  70  9 */ 1250, 1250, 1300, 1350, 1450, 1450, 1450, 1450, 
///*  80  6 */ 1250, 1250, 1250, 1350, 1450, 1350, 1350, 1350, 
///*  90  3 */ 1250, 1250, 1250, 1250, 1250, 1300, 1250, 1250, 
///* 100  1 */ 1250, 1300, 1250, 1300, 1250, 1250, 1250, 1250, 
///*  rpm       400   800  1200  1600  2000  2400  2800  3200 */

// COLD START: choke. MSGCHOKE contains a multiplier, in percent 0..N,
// that is applied to the target AFR based upon coolant temperature.
//
// The multiplier is scaled linearly from maximum at cold to zero warm.
// The YFA has an actual choke; this tells this code to not lean it out.
//
const int CHOKEMIN =               0;    // minimum value (no effect)
const int CHOKEMAX =               0.15; // maximum choke value 
const float CHOKE_LOWTEMP =       60;    // choke multiplier is 100% at this temperature or below
const float CHOKE_HIGHTEMP =     160;    // choke multiplier is 0% at this temperature or above
const float CHOKEAFRLIMIT =       12.00; // do not enrichen more than this

// EEPROM storage locations for the ELM and TAFR tables. 
// Table size is RPMBINS * KPABINS * 2 bytes plus
// mark and checksum.
//
const byte ELMVERSION =          100;
const unsigned ELMMARK =        3000;
const unsigned ELMCHECKSUM =    3001;
const unsigned ELMDATA =        3002;

const byte TAFRVERSION =         101;
const unsigned TAFRMARK =       3800;
const unsigned TAFRCHECKSUM =   3801;
const unsigned TAFRDATA =       3802;

// The master value for the YFA control loop, in milliseconds,
// that happens to be the spec for the YFA solenoid PWM
// period. 
//
const int YFARATE =              100;

// We begin the calcs for the next cycle this long before the
// start of each PWM pulse.
//
const int CALCLEAD =               7;

// Range of values for the YFA solenoid PWM, percentage.
//
const int MINPERIOD =              4;
const int MAXPERIOD =             95;

// INT smoothing factor and error gain limits. Smoothing factor is calculated
// from loop time and INT time constant.
//
const float MININTTC =            0.5;  // seconds
const float MAXINTTC =           20.0;  // seconds
const float MINERRORGAIN =        0.1;
const float MAXERRORGAIN =       25.0;
const float MINROC =              1.0;  // arbitrary units
const float MAXROC =             10.0;  // arbitrary units

class {

// Runtime copy of target AFR values. Loaded from EEPROM and
// initializable with the fixed table above.
//
uint16_t TAFR [KPABINS] [RPMBINS];
bool tafrChanged;

// Enleanment learn map. These are each cell's integrator history
// and integrated enleanment value, stored as FLOATSCALER, eg. enlean
// times 100, so maximum hardware enleanment of 100 (100 mS on time)
// is 10000 (100.00), which fits in uint16_t.
//
uint16_t ELM [KPABINS] [RPMBINS];
bool elmChanged;

const int REPORTTIMER =  0;
const int CALCTIMER =    1;
const int NUMTIMERS =    2;
SRTimer T;

SRSMPID PEDAL;       // PID-derived accelerator pump squirt detect
SRSmooth TAFRS;      // target-AFR smoother
SRSmooth RPM, AFR, TEMP, MANI;

// Sensor values used in the calcs.
//
float mani, rpm, temp, gAFR;

int row, col;        // manifold pressure vs. RPM cell location in maps
int enlean;          // current enleanment applied to carb
float tAFR;          // target AFR

// INT smoothing parameters. The loop time is fixed by the hardware at 100 mS,
// so the fastest smoother (smoothF=1) is 100 mS/10Hz. The smallest FLOASCALER value
// of 0001 (0.01), is 10 sec/0.1Hz.
//
float errorGain;     // INT error gain
float smoothF;       // INT smoothing factor
float maniROCT;      // manifold pressure rate of change (squirt) threshold

bool learn;          // will adjust ELM to close loop
bool choked;         // cold enrichment in effect
bool squirt;         // accell pump squirt; suspend learn
bool running;
bool eeSaveNow;      // set true at ignition-off

// Convert a FLOATSCALER packed decimal to float. xxyy to xx.yy.
//float toFloat (int n) { return (float)n / FLOATSCALER; }
#define toFloat(F) ((float)F / FLOATSCALER)

// Convert a float to a FLOATSCALER packed decimal. xx.yy to xxyy.
//unsigned fromFloat (float f) { return f * FLOATSCALER; }
#define fromFloat(F) (F * FLOATSCALER)

public:

// This is the "API" for access to these innards, used by the YFA code and
// the LCD editor. The 2D table is ROWS are pressure, COLUMNS rpm.
//
void initELMTable (void)                 { initELM(); }    // zero the tqble
void loadELMTable (void)                 { loadELM(); }    // load from EEPROM
void loadAFRTable (void)                 { loadTAFR(); }   // load from EEPROM
void loadAFRFixedTable (void)            { initTAFR(); }   // load fixed table

int getLearn (void)                      { return learn; }  // oring cellChanged will remove cell-change flicker of AFR/A--
int getChoked (void)                     { return choked; }
int getChoke (void)                      { return fetchDatum (MSGCHOKE); };
int getAFR (void)                        { return gAFR * FLOATSCALER; }

int getColLabel (int n)                  { return RPMBIN * n + RPMBIN; }
int getRowLabel (int n)                  { return KPABIN * n + KPABIN; }

// The tables start at RPMBIN and KPABIN and rpm and mani, from which
// row,col are calculated, are bounded to the minimum value. So table
// indexing requires subtracting xxxBIN.
//                                                             - 0..xxxBIN
int kpaToRow (int kpa)                   { return BOUND ((mani - KPABIN) / KPABIN,   0, MAXKPA / KPABIN); }
int rpmToCol (int rpm)                   { return BOUND ((rpm  - RPMBIN) / RPMBIN,   0, MAXRPM / RPMBIN); }

int getELMCell (int row, int col)        { return ELM  [row] [col]; }  // not bounds checked
int getAFRCell (int row, int col)        { return TAFR [row] [col]; }  // not bounds checked

int getTAFR()                            { return fromFloat (tAFR); }  // current interpolated value

int getRows (void)                       { return KPABINS; }
int getCols (void)                       { return RPMBINS; }

int getActiveRow (void)                  { return row; }
int getActiveCol (void)                  { return col; }

// Manual command only.
void saveTables (void)                   { elmChanged= tafrChanged= true; saveTAFR(); saveELM(); }

int setChoke (int n) {  // expects FLOATSCALER value
  n= BOUND (n, CHOKEMIN * FLOATSCALER, CHOKEMAX * FLOATSCALER);
  sendMessage (MSGCHOKE, n);
  return n;
}

// Cell contents are packed decimal (FLOATSCALER).
int setELMCell (int row, int col, int n) { 
  n= BOUND (n, 0, MAXPERIOD * FLOATSCALER);
  if (getELMCell (row, col) != n) {
    elmChanged= true;
    ELM  [row] [col]= n;
  }
}
  
// Cell contents are packed decimal (FLOATSCALER).
int setAFRCell (int row, int col, int n) { 
  n= BOUND (n, MINAFR * FLOATSCALER, MAXAFR * FLOATSCALER);
  if (getAFRCell (row, col) != n) {
    tafrChanged= true; 
    TAFR [row] [col]= n;
  }
}



void setup () {
  L.log (F("setup YFA"));
  pinMode (YFASOLPIN, OUTPUT);
  digitalWrite (YFASOLPIN, 0);
  pinMode (LED_BUILTIN, OUTPUT);

  T.begin (NUMTIMERS);
  T.setTimer (REPORTTIMER, 1000);
  T.setTimer (CALCTIMER, YFARATE - CALCLEAD);

  RPM.begin     ( 0.5, YFARATE * 0.001);    // rpm
  AFR.begin     ( 0.5, YFARATE * 0.001);    // air/fuel ratio
  MANI.begin    ( 0.5, YFARATE * 0.001);    // manifold pressure
  TEMP.begin    ( 1.0, YFARATE * 0.001);    // coolant temp (GM)

  TAFRS.begin   ( 1.0, YFARATE * 0.001);    // target AFR smoother (sans interpolation)
  PEDAL.begin   ( 2.0, YFARATE * 0.001);    // for squirt calc
  PEDAL.propGain (1);
  PEDAL.integGain (-1);

  loadELM();
  loadTAFR();
  enlean= 0;
  eeSaveNow= false;
}


void loop () {

  // At the start of each carburetor PWM pulse time, schedule the
  // calculation to complete a few mS before the next pulse.
  //
  if (YFAPWM (enlean) == true) {              // solenoid ON phase begins
    T.resetTimer (CALCTIMER);                 // defer calcs until...
  }
  if (T.timer (CALCTIMER) == false) return;   // ... timer fires.

  // Measure how long this code takes to execute.
  //
  unsigned long sTime= millis();


  learn= running=    fetchDatum (MSGRUNNING);
  rpm=  RPM.smooth  (fetchDatum (MSGRPM));                 // is integer
  temp= TEMP.smooth (fetchDatum (MSGENGINETEMP));          // is integer
  mani= MANI.smooth (toFloat (fetchDatum (MSGMANIPRESS))); // is FLOATSCALER
  gAFR= AFR.smooth  (toFloat (fetchDatum (MSGAFR)));       // is FLOATSCALER

  // Realistically, when running RPM will never be less than MINRPM (RPMBIN) and
  // manifold will never be less than MINKPA (KPABIN). Since we use these
  // values to index tables, bound them to the table size. *to-row and
  // *to-col offset table index calcs by the minimum. 
  //
  rpm=  BOUND (fetchDatum (MSGRPM), RPMBIN, MAXRPM);
  mani= BOUND (mani, (float)KPABIN, (float)MAXKPA);
  // gAFR not bounded; checked in calc.

  smoothF= (0.001 * YFARATE) /  BOUND (toFloat (fetchDatum (MSGINTTC)),         MININTTC,     MAXINTTC);
  errorGain=                    BOUND (toFloat (fetchDatum (MSGERRORGAIN)),     MINERRORGAIN, MAXERRORGAIN);
  maniROCT=                     BOUND (toFloat (fetchDatum (MSGMANIROCTHRESH)), MINROC,       MAXROC);

  // Derive table cell (row, column) from manifold pressure and RPM.
  // 
  row= kpaToRow (mani);  
  col= rpmToCol (rpm);

  // The nominal table cell is (row, col), but derive the target AFR using bilinear
  // interpolation amongst neighboring cells to get a closer value. This prevents
  // large jumps and hunting. At the positive edges of the table the interpolation
  // gets compressed.
  // 
  // (ELM corrections are still rough because the one cell is used for a range of
  // interpolated tAFR.)
  //
#define ROWNEXT (row + 1 < KPABINS ? row + 1 : row)
#define COLNEXT (col + 1 < RPMBINS ? col + 1 : col)

  tAFR= BI (row, col,   KPABIN, RPMBIN,   mani, rpm, 
    toFloat (TAFR [row]     [col]),     toFloat (TAFR [row]     [COLNEXT]),  // lower left, lower right
    toFloat (TAFR [ROWNEXT] [col]),     toFloat (TAFR [ROWNEXT] [COLNEXT])); // upper left, upper right

  // Choke is stored as integer percentage, eg. 0 to 10 (percent).
  // Choke is applied to tAFR over the cold-engine temperature range.
  // Cold choke tAFR multiplier is 1.0 - choke, hot multiplier is 1.0.
  //
  choked= false;
  if (temp < CHOKE_HIGHTEMP) {
    float ck= toFloat (fetchDatum (MSGCHOKE));              // maximum choke value (cold)
    float t= BOUND (temp, CHOKE_LOWTEMP, CHOKE_HIGHTEMP);   // temperature span of interest
    tAFR *= fmap (t,   CHOKE_LOWTEMP, CHOKE_HIGHTEMP,    1.0 - ck, 1.0);
    tAFR= BOUND (tAFR, CHOKEAFRLIMIT, MAXAFR);              // rich limit
    choked= true;
    learn= false;
  }

  // If manifold pressure jumps up quickly, eg. accel pump
  // squirt, suspend learn, which would lean out steady state 
  // when the squirt ends (overshoot). 
  //
  float d= PEDAL.pid (mani);
  if (d <= 1) d= 0;                             // clip off visual noise (debug)
  squirt= (d >= maniROCT);
  sendMessage (MSGMANIROC, fromFloat (d));

  // Calculate AFR error, measured to target. Positive values
  // mean too rich. (Will be disregarded if gAFR out of range.)
  //
  float error= tAFR - gAFR;

  // Learn mode (closed loop) occurs only during normal steady 
  // state operation. 
  //
  if ((gAFR < MINAFR) || (temp < CHOKE_HIGHTEMP) || squirt) learn= false;

  // The ELM table contains the most recent value of enleanment
  // for this cell (rpm, kpa). Correct for AFR error iteratively
  // for this (each) cell.
  //
  // The table is FLOATSCALER packed decimal integers. A small amount of 
  // precision is lost each iteration (.00xxxx).
  //
  float e= toFloat (getELMCell (row, col));       // current value
  if (learn) {
    e= ((e + error * errorGain) * smoothF) + (e * (1.0 - smoothF));  
    e= BOUND (e, 0, MAXPERIOD);                   // keep in range (and not negative)
    setELMCell (row, col, fromFloat (e));         // store packed float in table
    elmChanged= true;
  }
  enlean= e;                                      // for the hardware

  // Save tables to EEPROM when ignition is first turned off.
  //
  if (eeSaveNow) {
    eeSaveNow= false;
    sendMessage (IPCL_STATUSMESSAGE, STSAVING);
    saveELM();
    saveTAFR();
  }

  if (running && debug (DEBUGYFA)) {
    L.header (F("YFA"));
    Serial.print (learn ? F(" L") : F(" -"));
    Serial.print (F(" MAP="));  Serial.print (mani,0); 
    Serial.print (F(" rpm="));  Serial.print (rpm); 
    Serial.print (F(" ["));     Serial.print (row); Serial.print (F(",")); Serial.print (col); Serial.print (F("]"));
    Serial.print (F(" tAFR=")); Serial.print (tAFR,2);
    Serial.print (F(" gAFR=")); Serial.print (gAFR,2);
    Serial.print (F(" err="));  Serial.print (error,2);
    Serial.print (F(" enl="));  Serial.print (enlean);
    Serial.print (F(" d="));    Serial.print ((int)d);
    Serial.print (F(" exT="));  Serial.print (millis()  - sTime);
    Serial.println();
  }

  // DEBUG TOOL: If manual PWM is set, override calculated. This 
  // is get-out-of-jail-free should calcs become undrivable.
  //
  unsigned y= fetchDatum (MSGYFAPWM);
  if (y > 0) {
    enlean= y;
  }
}



messageV recvMessage (messageV id, unsigned nnn) {
  switch (id) {
    case IPCL_IGNITION:
      if (nnn == 0) {
        eeSaveNow= true;
      }
      break;

    default: break;
  }
  return MESSAGEOK;
}


private:

// Bilinear interpolation. Looking up real values x,y in an integer-addressed table
// necessarily involves loss of precision, eg. digits lopped off of the right to get to
// the closest cell (floor x, floor y). This address is nearly always rounded down
// except for x,y values with no fractional part (99.0, etc). So the "real" answer
// (if we could have an infinite lookup table) is located in the space defined
// by the addressed cell and +1 cell in each direction -- a four cell graph. This code
// interpolates into that space using the remainder from the rounded off (floor) 
// coordinates.
//
// Algebraic reductions from https://codereview.stackexchange.com/questions/100744/optimizing-bilinear-interpolation
// 
float BI (int row, int col, int rowSpan, int colSpan, float x, float y, float LL, float LR, float UL, float UR) {

  // The frac is how far were are from lower-left (LL) as 0,0
  // to each dimension. Our tables do not have row/col for the 0th
  // datums, to save table space for needless data.
  //
  //           |<-------------- fraction -------------->|
  //            |<------- remainder ------->|
  float xFrac= (x - rowSpan - (row * rowSpan)) / rowSpan;   // runs 0..<1
  float yFrac= (y - colSpan - (col * colSpan)) / colSpan;
  
  // Linearly interpolate between the two bottom cells of the four-cell graph.
//  float xIL= LL * (1.0 - xFrac) + (LR * xFrac);
    float xIL= xFrac * (LR - LL) + LL;            // algebraic reduction

  // Linearly interpolate between the two top cells of the four-cell graph.
//  float xIH= UL * (1.0 - xFrac) + (UR * xFrac);
    float xIH= xFrac * (UR - UL) + UL;            // algebraic reduction

  // Now linearly interpolate between these two intermediate values.
//  float v= (xIL * (1.0 - yFrac)) + (xIH * yFrac);
    float v= yFrac * (xIH - xIL) + xIL;           // algebraic reduction

//  if (running && debug (DEBUGYFA)) {
//    L.header ("YFA BI");
//    Serial.print (" x="); Serial.print (x,2);
//    Serial.print (" y="); Serial.print (y,2);
//    Serial.print (" row="); Serial.print (row);
//    Serial.print (" col="); Serial.print (col);
//    Serial.print (" xFrac="); Serial.print (xFrac,2);
//    Serial.print (" yFrac="); Serial.print (yFrac,2);
//    Serial.print (" xIL="); Serial.print (xIL,2);
//    Serial.print (" xIH="); Serial.print (xIH,2);
//    Serial.print (" v="); Serial.print (v,2);
//    Serial.println();
//  }
  return v;
}



// This runs the PWM state machine, 10 Hz. At the beginning of
// each cycle the solenoid is energized (if over the minimum
// pulse width) and timer (sTime) advanced to the off time.
//
bool YFAPWM (unsigned onTime) {
static int state = 0;
static unsigned long sTime = 0L;
static unsigned long eTime = 0L;

  unsigned long t= millis();
  if (t < sTime) return false;

  switch (state) {
    // ON phase begins; energize solenoid.
    case 0:
     if (onTime >= MINPERIOD) {
        digitalWrite (YFASOLPIN, 1);
        digitalWrite (LED_BUILTIN, 1);
        sTime= t + onTime;        // ON pulse width
        eTime= t + MAXPERIOD;     // end of cycle
        state= 1;

      } else {
        sTime= t + MAXPERIOD;     // stay off; same state
      }
      return true;
      break;

    // Deenergize solenoid.
    case 1:
      digitalWrite (YFASOLPIN, 0);
      digitalWrite (LED_BUILTIN, 0);
      sTime= eTime;
      state= 0;
      break;

    default:
      L.log ("YFA state machine error", state);
      state= 0;
      break;
  }
}





// Load the ELM table from EEPROM. If it can't be loaded
// (wrong/changed version, checksum error) initialize it. It will be
// rebuilt.
//
void loadELM () { 
int i, j;
uint16_t nn;

  if (EEPROM.read (ELMMARK) != ELMVERSION) {
    L.log (F("YFA ELM version changed"));
    initELM();
    return;
  }
  uint8_t checksum= 0;
  unsigned ee= ELMDATA;
  
  // This loads 16 bit words composed of two 8-bit bytes in Bigendian order.
  // Each byte is checksummed.
  //
  for (i= 0; i < KPABINS; i++) {                 // rows (KPa)
    for (j= 0; j < RPMBINS; j++) {               // columns each row (RPM)
      checksum += EEPROM.read (ee);
      nn= EEPROM.read (ee++);
      checksum += EEPROM.read (ee);
      nn <<= 8;
      nn += EEPROM.read (ee++); 
      ELM [i] [j]= nn;
    }
  }
  if (checksum != EEPROM.read (ELMCHECKSUM)) {
    L.log (F("YFA ELM table EEPROM checksum error"));
    initELM();
    return;
  }
  elmChanged= false;
  L.log (F("YFA ELM table loaded"));
}



// Load the target AFR table from EEPROM. If this fails
// load the flash version.
//
bool loadTAFR () { 
int i, j;
uint16_t nn;

  if (EEPROM.read (TAFRMARK) != TAFRVERSION) {
    L.log (F("YFA TAFR version changed"));
    initTAFR();
    return false;
  }
  uint8_t checksum= 0;
  unsigned ee= TAFRDATA;

  // This loads 16 bit words composed of two 8-bit bytes in Bigendian order.
  // Each byte is checksummed.
  //
  for (i= 0; i < KPABINS; i++) {                 // rows (KPa)
    for (j= 0; j < RPMBINS; j++) {               // columns each row (RPM)
      checksum += EEPROM.read (ee);
      nn= EEPROM.read (ee++);
      checksum += EEPROM.read (ee);
      nn <<= 8;
      nn += EEPROM.read (ee++); 
       TAFR [i] [j]= nn;
    }
  }
  if (checksum != EEPROM.read (TAFRCHECKSUM)) {
    L.log (F("YFA TAFR table EEPROM checksum error"));
    initTAFR();
    return false;
  }
  tafrChanged= false;
  L.log (F("YFA TAFR table loaded"));
  return true;
}


// Save the ELM table.
//
void saveELM () {
int i, j;
uint16_t nn;

  if (elmChanged == false) return;

  unsigned long t= millis();
  uint8_t checksum = 0;

  unsigned ee= ELMDATA;
  for (i= 0; i < KPABINS; i++) {
    for (j= 0; j < RPMBINS; j++) {
      nn= ELM [i] [j];
      EEPROM.write (ee++, nn >> 8);
      checksum += (nn >> 8);
      EEPROM.write (ee++, nn & 255);
      checksum += (nn &255);
    }
  }
  EEPROM.write (ELMCHECKSUM, checksum);
  EEPROM.write (ELMMARK, ELMVERSION);
  elmChanged= false;
  L.log (F("YFA ELM table saved to EEPROM, took"), millis() - t);
}


// Save the TAFR table.
//
void saveTAFR () {
int i, j;
uint16_t nn;

  if (tafrChanged == false) return;
  
  unsigned long t= millis();
  uint8_t checksum= 0;

  unsigned ee= TAFRDATA; 
  for (i= 0; i < KPABINS; i++) {
    for (j= 0; j < RPMBINS; j++) {
      nn= TAFR [i] [j];
      EEPROM.write (ee++, nn >> 8);
      checksum += (nn >> 8);
      EEPROM.write (ee++, nn & 255);
      checksum += (nn &255);
    }
  }
  EEPROM.write (TAFRMARK, TAFRVERSION);
  EEPROM.write (TAFRCHECKSUM, checksum);
  tafrChanged= false;
  L.log (F("YFA TAFR table saved to EEPROM, took"), millis() - t);
}

// Initialize the ELM map.
//
void initELM () {
int i, j;

  L.log (F("YFA ELM table initialized"));
  for (i= 0; i < KPABINS; i++) {
    for (j= 0; j < RPMBINS; j++) {
      ELM [i] [j]= 0;
    }
  }
  elmChanged= true;
}


// Initialize the TAFR map by copying the fixed table in.
//
void initTAFR () {
int i, j;
int x = 0;

  L.log (F("YFA TAFR table initialized from fixed table"));  
  for (i= 0; i < KPABINS; i++) {
    for (j= 0; j < RPMBINS; j++) {
      TAFR [i] [j]= FIXEDTAFR [i] [j];
    }
  }
  tafrChanged= true;
}

} YFA;

//