Help convert AD values according to lookup table

I need to figure out how to calculate the Air/Fuel ratio read by my car's wideband lambda.

The table looks like this.
Voltage AFR Voltage AFR Voltage AFR Voltage AFR
0.00 8.43 1.40 10.64 2.81 13.99 4.21 17.31
0.16 8.43 1.56 11.02 2.96 14.34 4.37 17.69
0.31 8.43 1.72 11.40 3.12 14.72 4.52 18.05
0.47 8.43 1.87 11.75 3.28 15.10 4.68 18.05
0.62 8.79 2.03 12.13 3.43 15.46 4.84 18.05
0.78 9.17 2.18 12.49 3.59 15.84 4.99 18.05
0.94 9.55 2.34 12.87 3.74 16.20 NA NA
1.09 9.90 2.50 13.25 3.90 16.58 NA NA
1.25 10.28 2.65 13.61 4.06 16.96 NA NA

Basically I need help to find a calculation which will convert the digital reading 0-1223 to fit this table.

Have you plotted this data? Is it anywhere near a straight line? How accurate does the interpolation need to be? That is, can one assume a straight line between any two points, for the purpose of calculating a value between known points?

My subjects are languages, culture and philosophy for a reason. I really don't get along with maths :). No I have not plotted them, but I'm on it now. I will post my findings here. Thanks for guiding me.

My subjects are languages, culture and philosophy for a reason. I really don't get along with maths

Well maybe if you just reached out and had a discussion with your car you could persuade it to adjust it's own lambda? :wink:

How was this table defined? What range of values represent normal operating conditions?

Isn't around a 14.7:1 air/fuel ratio a normally optimum stoichiometric ratio?

Lefty

Belive me, I've had many discussions with my car already, all to no avail :slight_smile:
Like retro says: 14.7:1 A/F ratio is optimum for non-additive fuels. Cars are normally tuned a little below this, to allow for bad fuel and other potential issues. Mine is around 12,7:1.

[edit]I've done the plotting again. The first plot was incorrect. This time the line looks very straight to me.
[/edit]

[edit] It pays to read the manual properly. This is what it says.[/edit]

UEGO Analog Output

The analog output from the UEGO controller is a linear dc voltage signal that varies
from 0.5 Vdc at 8.5:1 AFR Gasoline (0.58 Lambda) to 4.5Vdc at 18.0:1 AFR
Gasoline (1.22 Lambda) over the operating range of the X-WIFI. The signal is used for
sending information to a data logger or an engine management system like the AEM
EMS or F/IC.

The transfer function for the output is listed below.
AFR = 2.375(V) + 7.3125

For example, if the output is 2.0 Vdc, the AFR is 12.06:1
2.375 * 2.0 + 7.3125 = 12.06

The code I've come up with is:

float mixValue = 0 //variable to store the AFR value
float UEGO { //Function to read UEGO and convert to AFR
  r = 0;
      //Read pressure sensor
      for(byte i = 0; i < 5; i++) //Five readings are made
      {
      r += analogRead(mixPin)-offset; //Read voltage from the UEGO:
      }
      r /= 5; //Average five readings
              
      mixValue = (2.375*r  + 7,3125);   
   return mixValue;
}

What I'm worrying about with this is that I'm not using the correct data types.

Programmers often go to some lengths to convert floating point calculations to integer math to increase speed.

The data logger is speed sensitive. Should I attempt to convert as suggested?

Using something like lambda = (analogread(pin) /102) + 7.315; wouldn't be too far out providing your 5v line is really at 5v.

lambda should be declared as real .

The anlogread function is probably going to be the performance bottleneck but I've used similar code to read an AC waveform 1000 times a second and had to put a delay in to regulate it.

lambda = (analogread(pin) /[glow]102.0[/glow]) + 7.315;

The division should be float too, otherwise it will get only 10 discrete values. Bebbetufs code uses an averaging of 5 values so the optimization may be too fast :slight_smile:

Rewrote the function to be compacter while still using the averaging - didn't check the numbers -

float UEGO()  //Function to read UEGO and convert to AFR
{ 
  int r = 0;
  for(byte i = 0; i < 5; i++) //Five readings are made
  {
    r += analogRead(mixPin)-offset; //Read voltage from the UEGO:
  }
  return 0.475 * r + 7,3125;  // compacted the math here
}

float mixValue = UEGO(); //variable to store the AFR value

I realize I've forgotten that voltage is converted to digital. How can I solve this in my calculation?

NOrmally 0.0 .. 5.0V == 0 .. 1023 in the analogRead() function

==> float Volts = 5.0 * analogRead(pin) / 1023.0;

The range of values returned by analogRead, 0 to 1023, corresponds to the range of voltages input, 0 to Vref. Vref can be externally applied (to the Aref pin) or the internal Vcc voltage.

The actual voltage then is the analogRead value divided by 1023.0 times Vref, whatever Vref is for your situation.

Thanks to all of you who have replied.

There's one bit I did not quite get.

so the optimization may be too fast

Which optimization are you referring to? I'm going to have loads more code to be run during the loop, as well as a big menu. This is just one of the functions which will be called from the loop. The SD-card datalogger I'm hoping to build will run many more sensors, an external interrupt and hopefully a gps unit as well. My main concern is that in the end the loop will be too slow making me loose data.

Do you think I can drop the averaging to increase speed?

I'm not sure how fast you need this system to be but I have a few suggestions that will speed things up.

  1. You may want to consider using a sliding window and taking analog samples on a timer interrupt. (EDIT: I forgot to add that depending on the characteristics of your noise, a median filter may work better)

  2. Floating point is SLOW and should usually be avoided when doing signal processing(which is what you're doing) on a chip like the atmega. You could either convert your algorithm to use only fixed point arithmetic (don't just cast everything to an int and call it good). This can be a bit tricky to get right and I won't do it for you because I don't want to have any liability if you blow up your engine by running too lean.

What is done in industry (including your car's ECU) is to compute that function for every possible input value (in this case it's only 1024 values) and store the answers in a look-up-table stored in flash. I'm not exactly sure what type your output needs to be but if it needed to be a float (probably a wost case) then thats still only 4k of flash needed, heck that would even fit in the EEPROM (of the mega 2560) and could be computed (only once) the first time you run your code.

Thanks for your thorough explanation.

The lambda has a rs232 output which I will read from my laptop when actually doing any tuning. I will only use the analogue out for a lcd display, an alarm buzzer and for logging. It will give me a warning in case fuel the pressure drops or an injector clogs. I will have knock sensing as well.

I would very much like to see an example of how this could be done as I'm so new to programming I find it very hard to wrap my head around concepts without examples to think with.

so the optimization may be too fast

I referred to the formula lambda = (analogread(pin) /102.0) + 7.315; which does no averaging of five readings, so it is much faster but it will also be "noisier"

I see.
I didn't think of that as an optimization, but it is of course an optimization of the code I posted. :-[

(note when reading this: when I write L' and Lprime I am referring to the same variable I just wrote Lprime to be compatible with C naming conventions)

When implementing algorithms using integral types (int, char, long, etc) you need to pay CLOSE attention to your significant figures. Take for example the equation:

[b]L = (x /102.0) + 7.315[/b]

When doing integer math, you loose everything after decimil point. We know 0<=x<1024 (its important here to know the range of all variables otherwise you may loose too many digits or worse yet overflow)

Given our range for x, we get

[b]7.315<=L<17.354[/b]

If we just cast everything to ints and called it a day we would get 11 possible values, 7,8,9, ... , 17, which is probably not accurate enough for AFR.

If instead we compute L' := L*1000 then

[b]Lprime= (x *1000 /102) + 7315[/b]

Now we have as our range of values, 7315, 7325,..., 17344 (note there are only 1024 possible values because x can only take on 1024 values)

There is STILL one more problem x*1000 can be as large as 1,000,000~=2^20, which is too big for an int in our environment (doe!). C/C++ is a funny language in that int, and long don't have fixed sizes for every compiler. In AVR gcc and int is 16 bits and a long is i don't know how long (must be at least as long as an int). For this reason I always use the types in stdint.h

rant aside, we must cast x as a uint32_t (32bit unsigned integer)

[b]Lprime= ((uint32_t)x) *1000 /102 + 7315[/b]

It may be really tempting to do the following (i've stricken it out to emphasize its wrong)

[s][b]Lprime= x /102*1000 + 7315[/b][/s]

which is mathematically equivalent and doesn't require any 32bit intermediate values! BUT don't do it! note now that (x/102) can take values 0,1,...,10! and your back to the bad case you started with!

Now we have L'. How do we get L? we can either do the following

[b]L = (float)Lprime/1000.0;[/b]

but we're back to floating point math and if we can avoid it completely then we don't have to link to the libraries and we save code space and time.

Often times L' can be worked on as is and what you do with L' from here is application specific

If you just want to print you can print
Lprime/1000, then '.', then (Lprime%1000)

Thanks for explaining all that to me. I will take a good close look at it and have a mate of mine do the same as well as this Lprime= x /102*1000 + 7315 was exactly what we were planning on doing.

I did not know you could do this when printing. Neat!

lcd.print (Lprime%1000);