Jesse's electronic automixer

At Argos 2022, Jesse gave a short talk on an automixer he was working on. Is there any new information on how that is going?

7 Likes

What is required is someone who is willing to write a program for Arduino that uses the PID function.

This is beyond my skills in C++ and my understanding of the math component is shaky.

This is a very simple and elegant explanation.
It does not go into derivatives and the calculus of closed loop controls with feedback.

This makes me wonder if you need to understand whats happening under the hood anymore to program this stuff as long as you know C and kind of understand closed loop control.

Ya now that your feeling good about how simple it is.
Watch this and see what you think.

3 Likes

My code works well. You only need an O2 sensor input but this will require a wide band sensor with a full 5 volt signal. I have not figured out how to make a narrow band work as the signal does not have the resolution even if it is amplified.

This maps the O2 sensor to a range of numbers or the full bytes available. It is also mapped inverted and both have their own variable. Now I can compared this variable with the actual reading and assign a delay too it. This delay is set in-between the angle position of the servo. Everytime the servo moves it moves only 1 position angle waits for the delay and then proceeds each time reading new values on the fly. So I can control how fast the servo moves based on how far out of range the mixture is. If its way out of range you want that servo to move fast. But once you get close to the optimal mixture you need to slow it down otherwise it will over shoot and go into a hunting mode where it keeps overshooting each time. When it is in range and then goes slightly out of range you also want those movements very slow as it dont take much to over shoot.

#include <Servo.h>
Servo myservo; // create servo object to control a servo

#include <Servo.h>

Servo myservo; // create servo object to control a servo

//VG 02 Wide Band Mixture Controller V1.3
//Craeted by Matt Ryder, Vulcan Gasifier LLC / Thrive Energy Systems May 5, 2015
//website; https://www.thriveoffgrid.net/
//Prior edit; June 22, 2015
//Change log
//April 30,2017 - Todd Harpster
// added contraint to calculated delays after mapping to keep them in range
// read the O2 sensor just once per loop
// add table of expected delays based upon O2 sensor value
//April 30, 2017 - Todd Harpster
// v2 control loop
// refactored code to use mode variable and switch statement to

// This code requires an AEM O2 sensor kit with LSU 4.9 sensor Part # 30-4110

// These constants won’t change:
const int O2SensePin = A1; // pin that the O2 sensor is attached to
const int O2SwtPin = A2; //Pin Auto Mode Switch is attached to
const int O2ManPin = A0; // Reads the O2 Manual Adjust Potentiometer on pin A1

// constants for our mode values
const int Mode_Override = 1;
const int Mode_Rich = 2;
const int Mode_Stoichiometric = 3;
const int Mode_Lean = 4;

//tunable parameters below
const int RichCon1 = 700; // Threshold limit for Rich gas condition
const int LeanCon1 = 850; //Threshold limit for lean gas condition
//
const int MinTuningServoPosition = 10; // never fully close the valve in O2 loop mode
const int MaxTuningServoPosition = 180; // wide open

int mode = 0; // loop operating mode
int pos = 0; // variable to store the servo position
int val; // variable to read the value from the analog pin
int lastpos = val; // variable to store the servo last position
int O2SwitchState = 0; // variable for reading the Fuel Mixer Mode status

void setup()
{
analogReference(INTERNAL);
myservo.attach(13); // attaches the servo on pin 9 to the servo object

// Need to create a set point for servo start position after manual tuning
{
val = analogRead(O2ManPin); // reads the value of the potentiometer (value between 0 and 1023)
val = map(val, 0, 1023, 0, 180); // scale it to use it with the servo (value between 0 and 180)
myservo.write(val); // sets the servo position according to the scaled value
lastpos = val; // sets lastpos to equal the position of the manually set position
delay(10); // waits for the servo to get there
}
}

//
// O2 Sensor Reading Delay
// ================= ====================
// Manual 10
// Rich 0 - 700 variable 500 - 800
// Stoichi 701-899 500
// Lean 900 - 1023 variable 800 - 450
//

void loop()
{
// read the state of the pushbutton value:
O2SwitchState = digitalRead(O2SwtPin);
// read the value of the O2 sensor:
//Todd - read the O2 sensor once for this loop and store in sensorReading
int sensorReading = analogRead(O2SensePin);

//variable to hold mode for this time through the loop
int mode = Mode_Stoichiometric; // assume O2 reading is not too rich or not too lean, just right
int delayms = 500; // 500 ms delay as a default value - it case it doesn’t get changed below

if (O2SwitchState == LOW) {
mode = Mode_Override;
} else {
if ( sensorReading <= RichCon1 ) {
mode = Mode_Rich;
}
if (sensorReading >= LeanCon1 ) {
mode = Mode_Lean;
}
}

switch (mode) {
case Mode_Override:
val = analogRead(O2ManPin); // reads the value of the potentiometer (value between 0 and
1023)
val = map(val, 0, 1023, 0, 180); // scale it to use it with the servo (value between 0 and 180)
myservo.write(val); // sets the servo position according to the scaled value
lastpos = val; // sets lastpos to equal the position of the manually set position
delayms = 10; // waits for the servo to get there
break;

case Mode_Rich:
// move the air mixture servo to lean out by adding air
lastpos = constrain(lastpos+1, MinTuningServoPosition, MaxTuningServoPosition); // add 1 but make
sure it stays in range
myservo.write (lastpos); // tell the servo where to go
val = map(sensorReading, 0, RichCon1, 100, 400); // Richer mixtures get shorter delay
val = constrain( val, 100, 400); // Keep values between 100 and 400 ms
delayms = val + 400; // increase delay so minimum is 500 ms - max is 800 ms
break;

case Mode_Stoichiometric:
delayms = 500; // no need to move servo set delay to check back in 500 ms
break;

case Mode_Lean:
// move the air mixture servo to decrease air to make richer
lastpos = constrain(lastpos-1, MinTuningServoPosition, MaxTuningServoPosition); // subtract 1 but
make sure it stays in range
myservo.write (lastpos); // tell the servo where to go
val = map(sensorReading, 1023, LeanCon1, 450, 800); // the leaner the mixture the shorter the delay -
need to adjust quickly
val = constrain(val, 450, 800); // keep the delay between 450 and 800 ms
delayms = val;
break;

otherwise:
//this shouldn’t happen
break;
}

//wait for a little bit before doing this again
delay(constrain(delayms,250,1250)); // use contrain to be completely sure delay value is
reasonable
}

5 Likes

Then there is this code. This has additional modes for running the blower and an electronic ball valve for the flare cup. Engine start mode that relieves the engine coil ground and allows for manual control the mixer valve and then a start button. Then automix mode with integrated timers for two relays to control a grate and hopper agitator. Then a shut down mode.

This code is interfaced with a simple mode button. Each time you press it; it goes to the next mode of operation.

// Thrive OS Basic 2020 Version 0.1
//Created (9/28/2019) By Matt Ryder CEO Thrive Energy Systems

// Last Edit (creation)
// Overview: Single controller machine controls featureing static timer controls
// for grate and hearth agitation, controler initiated Blower on/off Controls,
// engine coil grounding, engine starter and integrated AFR mixer controler
// Interface controls are mode push button, engine starter button, and Potiometer control
// for manual AFR Servo adjustment

#include <Servo.h>

// Pin Inputes
const int AEM_Sig = A0;// AEM Signal Wire Input
const int AFRS_Ma = A1;//Air Fuel Ration Servo Manual Adjust Signel Wire
const int Mode = A2; // Mode Button Input Signal Wire

// Pin Outputs
const int Mode_LED = 13;// Output to Mode Button LED to show state of controls
const int Blower = 4;//Blower on / off (Relay 1)
const int Grate_Ag = 7; // Grate Agitator motor On/Off (Relay 2)
const int Hearth_Ag = 8; // Hearth Agitator motor On/Off (Relay 3)
const int Eng_Coil = 12;// Engine Coil Ground

Servo AFRS;//Air Fuel Ratio Servo

// Variables
int AEM_Read = 0;//Store AEM reading
int Mode_Read = 0;//Set Viavble to determin mode state
int AFR_MA_Read = 0;// Store Manual Input Reading
int AFRS_Pos = 0;//Stores AFRS Pos
int ModeState = 0;//Stores the Mode State from Input
int mode = 0;//Stores the Switch Mode
int LED_State = 0;//Mode button state of the LED (off = 0) (On = 1)
int AFR_Opt_State = 0;//We change this state once we shift into AFR_Optimal mode to delay readin intervals
int ProgramCount = 0;
int ProgramMode =0;
int ProgramActive =0;

//Agitator System
int Hearth_Ag_State = 0;//Varible to select on or off timer intervals
int Grate_Ag_State = 0;//Varible to select on or off timer duration

// Tuneable Pareameters
const int MinTuningServoPosition = 35; // AFRS Postion Limit (Closed)
const int MaxTuningServoPosition = 165; // AFRS Position Limit (Open)
const int Rich_Condition = 600; // Any reading bellow set value is Rich
const int Lean_Condition = 650; // Any reading above Set Value is Lean

//Switch Modes
const int Program = 1;
const int Ignition = 2;
const int EngineStart =3;
const int AFR_Rich = 4;
const int AFR_Optimal = 5;
const int AFR_Lean = 6;
const int ShutDown = 7;

// Timer Variables

//led blink mode status
unsigned long LED_Status_ReSet = 0;// Variable to reset comparison time to currentmillis time
const long LED_Ignition_Interval = 250; // Interval to blink Mode button LED
const long LED_EngStart_Interval = 750;//Inerval to blinkj Mode Button LED
//AEM automix
unsigned long Servo_Delay_Reset = 0; // Variable to reset Comparison time to Currentmillis time
long Rich_Delay_Interval = 0;// Store mapped comparison of AEM Sig reading to range of delay
long Lean_Delay_Interval = 0;// Store mapped inverted comporison of AEM Sig reading to range of delay

//AFR Optimal Mode
unsigned long AEM_Read_Reset = 0;
const long AEM_Read_Delay = 3000;

//Agitator System
unsigned long Grate_Interval_ReSet = 0;
const long Grate_On_Interval = 500;
const long Grate_Off_Interval = 60000;

unsigned long Hearth_Interval_ReSet = 0;
const long Hearth_On_Interval = 3000;
const long Hearth_Off_Interval = 180000;

//Program Mode

unsigned long Program_Interval_ReSet = 0;
const long Program_On_Interval = 500;
const long Program_Off_Interval = 800;

void setup() {

Serial.begin(9600);

pinMode(AEM_Sig, INPUT);
pinMode(AFRS_Ma, INPUT);
pinMode(Mode, INPUT);
pinMode(Mode_LED, OUTPUT);
pinMode(Blower, OUTPUT);
pinMode(Grate_Ag, OUTPUT);
pinMode(Hearth_Ag, OUTPUT);
pinMode(Eng_Coil, OUTPUT);
AFRS.attach(9);

digitalWrite(Mode_LED, LOW);
digitalWrite(Blower, LOW);
digitalWrite(Grate_Ag, LOW);
digitalWrite(Hearth_Ag, LOW);
digitalWrite(Eng_Coil, LOW);

AFRS_Pos = (45);
AFRS.write(AFRS_Pos);

}

void loop() {

AFR_MA_Read =analogRead(AFRS_Ma);
Mode_Read = digitalRead(Mode);

unsigned long CurrentMillis = millis();

if (AFR_Opt_State == 0){// In AFR Optimal mode this byte is changed to 1 and set to delay and swithes back to 0 so that it samples the signal
AEM_Read = analogRead(AEM_Sig);
delay(15);
}

if (Mode_Read == 1 && ProgramMode == 0){
ModeState = (ModeState +1);
delay(750);
}

if (ModeState >= 6){
ModeState = (0);
delay(15);
}
else if (ModeState == 1){
//Program_Interval_ReSet = CurrentMillis;
ProgramMode = 1;
mode = Program;
delay(15);
}
else if (ModeState == 2){
mode = Ignition;
delay(15);
}
else if (ModeState == 3){
mode = EngineStart;
delay(15);
}
else if (ModeState == 4 && AFR_Opt_State == 0 && AEM_Read < Rich_Condition){
mode = AFR_Rich;
delay(15);
}
else if (ModeState == 4 && AEM_Read > Rich_Condition && AEM_Read < Lean_Condition){
mode = AFR_Optimal;
delay(15);
}
else if (ModeState == 4 && AFR_Opt_State == 0 && AEM_Read > Lean_Condition){
mode = AFR_Lean;
delay(15);
}
else if (ModeState == 5){
mode = ShutDown;
delay(15);
}

//Agitator Controls
if (ModeState >= 2 && Grate_Ag_State == 0 && CurrentMillis - Grate_Interval_ReSet > Grate_On_Interval){
digitalWrite(Grate_Ag, LOW);
Grate_Ag_State =1;
Grate_Interval_ReSet = CurrentMillis;
delay(15);
}
else if (ModeState >= 2 && Grate_Ag_State == 1 && CurrentMillis - Grate_Interval_ReSet > Grate_Off_Interval){
digitalWrite(Grate_Ag, HIGH);
Grate_Ag_State =0;
Grate_Interval_ReSet = CurrentMillis;
delay(15);
}
if (ModeState >= 2 && Hearth_Ag_State == 0 && CurrentMillis - Hearth_Interval_ReSet > Hearth_On_Interval){
digitalWrite(Hearth_Ag, LOW);
Hearth_Ag_State =1;
Hearth_Interval_ReSet = CurrentMillis;
delay(15);
}
else if (ModeState >= 2 && Hearth_Ag_State == 1 && CurrentMillis - Hearth_Interval_ReSet > Hearth_Off_Interval){
digitalWrite(Hearth_Ag, HIGH);
Hearth_Ag_State =0;
Hearth_Interval_ReSet = CurrentMillis;
delay(15);
}

//Switch Modes
switch (mode){

   case Program:
    
    if (Mode_Read == 1 && ProgramActive == 0 && ProgramMode == 1 &&  CurrentMillis - Program_Interval_ReSet > Program_On_Interval){

      ProgramCount = (ProgramCount +1);
      //ProgramActive =1;

      Program_Interval_ReSet = CurrentMillis;          
      Serial.println(ProgramMode);
      Serial.println(ProgramCount);
      Serial.println("Entering ProgramMode");
      delay(50);
    }
    else if (Mode_Read == 0 && ProgramMode == 1 && ProgramCount < 10){
      ProgramMode = 0;
      ModeState = 2;
      ProgramCount = 0;
      Program_Interval_ReSet = CurrentMillis;           
      Serial.println ("Program Mode Aborded");
      delay(50);
    }

    if (ProgramCount > 1 && Mode_Read == 0 ){
           
      ProgramActive=1;
      Serial.println("ProgramMode Active");
      ProgramMode = 3;          
      delay(50);        
    }
    if (ProgramActive == 1 &&  Mode_Read == 1){
      ProgramMode = 0;
      ProgramCount = 0;
      ModeState = 6;
      ProgramActive = 0;
      digitalWrite(Mode_LED, LOW);
      digitalWrite(Eng_Coil,LOW);
      digitalWrite(Hearth_Ag, LOW);
      digitalWrite(Grate_Ag, LOW);          
      AFRS.write(45);          
      Serial.println("ProgramMode Off");
      delay(50);
    }          
    break;      

  case Ignition:
    Serial.println("Mode Ignition");      
    AFRS.write(45);
    digitalWrite(Blower,HIGH);
    if (LED_State == 0 && CurrentMillis - LED_Status_ReSet > LED_Ignition_Interval){
      digitalWrite(Mode_LED,HIGH);
      LED_State = 1;
      LED_Status_ReSet = CurrentMillis;
    }
     else if (LED_State == 1 && CurrentMillis - LED_Status_ReSet > LED_Ignition_Interval){
      digitalWrite(Mode_LED,LOW);
      LED_State = 0;
      LED_Status_ReSet = CurrentMillis;
    }

    break;      

   case EngineStart:
    Serial.println("Mode Engine Start");       
    digitalWrite(Blower,LOW);
    digitalWrite(Eng_Coil,HIGH);
    AFRS_Pos = map (AFR_MA_Read, 0, 1023, 40, 140);
    AFRS.write (AFRS_Pos);
    if (LED_State == 0 && CurrentMillis - LED_Status_ReSet > LED_EngStart_Interval){
      digitalWrite(Mode_LED,HIGH);
      LED_State = 1;
      LED_Status_ReSet = CurrentMillis;
    }
    if (LED_State == 1 && CurrentMillis - LED_Status_ReSet > LED_EngStart_Interval){
      digitalWrite(Mode_LED,LOW);
      LED_State = 0;
      LED_Status_ReSet = CurrentMillis;
    }
   
    break;

   case AFR_Rich:
    Serial.println("Mode Rich");       
    digitalWrite(Mode_LED,HIGH);       
    Rich_Delay_Interval = map (AEM_Read, 0, Rich_Condition, 450, 800);
    if(AFRS_Pos < 140 && AEM_Read < Rich_Condition && CurrentMillis - Servo_Delay_Reset > Rich_Delay_Interval){
      AFRS_Pos = (AFRS_Pos +1);
      AFRS.write(AFRS_Pos);
      Servo_Delay_Reset = CurrentMillis;
      Serial.println(AFRS_Pos);
    }
   
    break;

   case AFR_Optimal:
    Serial.println("Mode Optimal");       
    digitalWrite(Mode_LED,HIGH);        
    if(AFR_Opt_State == 0 && CurrentMillis - AEM_Read_Reset > AEM_Read_Delay){
      AFR_Opt_State = 1;
      AEM_Read_Reset = CurrentMillis;
    }
    else if (AFR_Opt_State == 1 && CurrentMillis - AEM_Read_Reset > AEM_Read_Delay){
      AFR_Opt_State = 0;
      AEM_Read_Reset = CurrentMillis;          
    }
     
    break;
    
   case AFR_Lean:
    digitalWrite(Mode_LED,HIGH); 
    Serial.println("Mode AFR Lean");       
    Lean_Delay_Interval = map (AEM_Read, Lean_Condition,1023, 800, 450);
    if(AFRS_Pos > 40 && AEM_Read > Lean_Condition && CurrentMillis - Servo_Delay_Reset > Lean_Delay_Interval){
      AFRS_Pos = (AFRS_Pos -1);
      AFRS.write(AFRS_Pos);
      Serial.println(AFRS_Pos);
      Servo_Delay_Reset = CurrentMillis;
      
    }
     
    break;

   case ShutDown:
    digitalWrite(Mode_LED, LOW);
    digitalWrite(Eng_Coil,LOW);
    digitalWrite(Hearth_Ag, LOW);
    digitalWrite(Grate_Ag, LOW);          
    AFRS.write(45);
    ModeState = 0;
    Serial.println("Mode Shut Down");
    break;                             
    

}

}

4 Likes

Matt is there any way this program would work for an electronic throttle body?

5 Likes

As long as there is a servo that moves it, I dont see why it wouldnt.

6 Likes

Yeah, a drive by wire throttle body. Something along the lines of this one which seems affordable. I know at least for the GM TB’s that two of those pins are the Pos and Neg in. I want to say it’s a 0-5v range I’ll have to play with one of the ones I’ve scrounged.

Tecoom 12631016 Electronic Throttle Body Assembly Compatible with 2008-2012 Buick Allure Lacrosse Chevrolet Colorado Impala Hummer H3 Pontiac 3.7L 5.3L https://a.co/d/grNhA03

6 Likes

Matt, at my age it makes my head hurt reading all that code. :grimacing:

6 Likes

My brain hurts too Don.
Thirty years ago this was something you had to do and adjust with amplifiers, and add capacitors and potentiometers. You had to do a lot of high level math to calculate the stability of the thing ( would it hunt, was it not sensitive enough, did it take to long ).
By the time i left college smoke came out of my ears.
I never used it again, BUT there were industrial applications where the tech guys would add pieces of software like this to fine tune a process.
It was nice to be able to look at the PLC logic and see AH HA they did a PID loop and thats why the system is doing that…

And if it was not working right you could get on the phone with the tech guy and tell him what you see it doing in real life and what I think he should try and adjust to make the system settle down and run consistently.

Now the processing power of something like the Ardiuno lets you right simple code and see how it behaves.
And if you learn to program in C language the controller runs the program in C language so you can see and make changes on the fly.

In my day to make a micro controller do this you were literally working at a machine code level or you were building stuff from discrete components ( transistors and resistors and parts ).

Now anyone with the time and ambition can do stuff even if they are foggy on the math.
You can experiment without completely redesigning every time you want to make a change.

Here is the code for the PID loop on the ballance beam.
The reason I am pressing this point is the PID instruction has a faster response and self correction for over shoot and under shoot.

Idealy you want some dead band in there.
You don’t want the controller to make constant changes in an attempt to achieve perfection.
This sketch does not have that and the servo will be in constant motion ( small but constant )trying to get to perfection.

Its also has a constant scan rather than waiting.
This might be problem if your trying to do more than one thing at a time.
On an engine fuel management application your going to have to add an interrupt or scheduled a jump to subroutine every now and then to do any other housekeeping function.
Also some code in here you don’t need, the serial prints and the debug looks like you can make adjustments to it on the fly ( maybe you want that I don’t know )

If you look at the is graph you can see how the parameters work and what the program is trying to do.
It is trying to get the output in sync with the input.
If it over shoots it corrects if it undershoots it corrects until it matches as perfectly as the limitation of the hard can reach and it settles at the set point
If that set point lines moves the output tries to follow.
By adjusting the P, I , and D parameters we can change how aggresively it chases the setpoint with the output and how quickly it tries to match it with the goal have a minimum of over and undershoot at the fastest reaction time possible. ( and of course the minumim of dead band )
Dead band is like your thermostat at home it turns the furnace on and off to regulate the temperature.
If this was done with a PID rather than on and off and wait for changes it would keep the furnace running at all times and increase and decrease the amount of heat put out so the furnace is constantly running at the minimum level of heat output to keep the room as close to exactly how hot you want it ( without changes you can feel )

image
Here is a flow chart to show what that looks like.
image

Step 6: Arduino Code

// Libraries
#include <SharpIR.h>
#include <Servo.h>

// Defines
#define PIN_SERVO 3
#define PIN_SENSOR 0
#define PIN_LED 13
#define DEBUG_ACTIVE 1
#define IR_MODEL 1080

// Running average
const int numReadings = 3;
int readings[numReadings]; // The readings from the analog input
int readIndex = 0; // The index of the current reading
double total = 0; // The running total
int sensorRunningAverage = 0;

// Constants
const double Kp = 1;
const double Ki = 0.1;
const double Kd = 15;
const double sampleTime = 100; // ms
const double setpoint = 20; // cm
const int sensorMax = 57; // cm

// Floating variables
double error = 0;
double oldError = 0;
double diffError = 0;
double sumError = 0;
unsigned long lastTime = 0;
unsigned long standbyTime = 0;
unsigned long runningTime = 0;
double u = 0; // Output of the PID controller
int servoOutput = 0; // Output to the servo
double dis = 0;

// Functrions
Servo servo;
SharpIR SharpIR(sensorRunningAverage, IR_MODEL);

void setup() {
servo.attach(PIN_SERVO);
pinMode(OUTPUT, PIN_LED);

startup();

for (int thisReading = 0; thisReading < numReadings; thisReading++) {
readings[thisReading] = 0;
}
lastTime = millis();

if (DEBUG_ACTIVE) {
Serial.begin(9600);
}

}

void loop() {
sensorRunningAverage = getSensorRunningAverage(analogRead(PIN_SENSOR));
dis = SharpIR.distance(); // this returns the distance to the object you’re measuring

/How long since we last calculated/
unsigned long now = millis();
double timeChange = (double)(now - lastTime);
lastTime = now;

if (dis >= sensorMax) {
error = 0;
sumError = 0;
diffError = 0;

standbyTime = millis() + 2000;

}
else if (standbyTime < now) {
error = dis - setpoint;
sumError += (error * (timeChange / 1000));
diffError = error - oldError;
oldError = error;
}

if (sampleTime + runningTime < now) {
if (!(error <= 1 && error >= -1 && servoOutput == 26 && diffError <=0.5 && diffError >=-0.5)) { //tweaking of the regulator so that it stands still if the ball is within ±1 Cm.
u = (Kp * error) + (Ki * sumError) + (Kd * diffError);
}
servoOutput = constrain(-u + 26, 0, 65);
servo.write(servoOutput);

runningTime = millis();

}
if (DEBUG_ACTIVE) {
printStatus();
}
}

void startup() {
for (int i = 75; i >= 15; i–) {
servo.write(i);

if ((i / 2 & 1) == 0) {
  ledPinOn(PIN_LED);
} else {
  ledPinOff(PIN_LED);
}
delay(50);

}
}

int getSensorRunningAverage(int sensorValue) {
total = total - readings[readIndex];
readings[readIndex] = sensorValue; // read from the sensor:
total = total + readings[readIndex]; // add the reading to the total:
readIndex = readIndex + 1; // advance to the next position in the array:
if (readIndex >= numReadings) { // if we’re at the end of the array…
readIndex = 0; // …wrap around to the beginning:
}
float average = total / numReadings; // calculate the average:
return average;
}

void printStatus() {
Serial.print(“Distance: “);
Serial.print(dis);
Serial.print(”\t”);

Serial.print(“Error: “);
Serial.print(error);
Serial.print(”\t”);

Serial.print(“Sum error: “);
Serial.print(sumError);
Serial.print(”\t”);

Serial.print(“Diff error: “);
Serial.print(diffError);
Serial.print(”\t”);

Serial.print(“u(t): “);
Serial.print(u);
Serial.print(”\t”);

Serial.print("Output: ");
Serial.println(servoOutput);
}

void ledPinOn(int led) {
digitalWrite(led, HIGH);
}

void ledPinOff(int led) {
digitalWrite(led, LOW);
}

4 Likes

It does not have ability to make smaller and smaller adjustments to stay within the range of the standard narrow band of the O2 sensor.
Or put another way it has more dead band than the sensor has operational range.

There are work arounds.
You might use an mass airflow sensor and throttle possition sensor then add some tables for the controller to jump to a setting that close and then allow the O2 sensor to fine tune.

It might just be easier to stick with the wide band and avoid all this.

4 Likes

If you guys wondered what it might look like to try and do this without a computer this a simplified electronic diagram of what you need.

The triangles are called OP-Amps they are literally amplifiers on a chip that give you a very high gain with very high precision to detect condition signals and add things together produce corrections and finally drive an output. ( this is just a simple pictorial diagram, in real life there would be a hell of a lot of connections to power all these components not shown for the sake of simplicity… if you want to call this simple! )

I remember spending weeks designing something like this that would keep a motor at a constant speed as the load changed.
And I had to design the drive for the motor too!
And I had a lot of trouble trying to learn how to do this not a word of a lie this is tough.

In our case the motor would be the servo, the feedback resistor would be the O2 sensor and the set point would be a pot we adjust to fuel mixture.
But its all there P I and D and maybe this is a better way to understand it all.

Right now I don;t have time, but I will get back to this…

A bit of an explanation here as best as i can give.

U1 is an operational amplifier ( from here on this chip will be called OP amp and likely this is in a chip that contains several for simplicity of wiring ).
U1 compares the set point voltage + ( setting in our case would be an input voltage scaled to the fuel mixture setting I was through a variable resistor and power suply divider ect no shown ).
The - input is the feedback signal and U1 compares the two outputs its voltage of the combined and conditioned new signal to U2 and U3.
U2 proportional will decide how much voltage to out out in order to force change.
U3 will act as a ramp generator to integrate the signal ( so it responds at the correct speed )
U4 Is derivative a mathematical calculus thing that causes smoke to come out of my ears.
It functions kind of like a dash pot to dampen oscillations and make this settle down and behave.
U5 is another comparator like U1 that drives the final output and it compares the outputs of U2 and U3 with feedback from U4 to create an output that will control the motor from the push pull circuit .

You can’t see the math here, but let me tell you this is University level stuff and you needed to take adavanced math in high school to get here…

I can already see some changes easily made.
This was probably designed with the 741C op amp in mind and need a + and - 12 volt power suply.
A 324M chip might do it with only 15 volts + and we can dump the push pull circuit at the end and feed that signal directly to a servo with some condition and maybe a third op amp.

I am probably going to follow up and try and modify this with some help with the folks at the Electrical and Electronics engineering forum.
If they agree I will post some links, at the end we might have a working circuit.
The last stage would be to have a nice Chinese man manufacture some boars and you guys interested in this stuff could order and build this creature…

5 Likes

Yup this code with the wideband is solid. Yeah its not worth beating yourself up trying to get a narrow band to work.

5 Likes

Hurts my head too and I wrote it. lol. Whats really fun is to write a piece of code and then come back a few years later and try to figure out what you wrote. lol

7 Likes

I like that Op amp circuit so much I decided to see if I could get some help to build a functional one like it on another forum full of much smarter electronics people than me…

SO if you want to follow a long and see, I started over here.

And if your not well I will re post any information .

3 Likes

Some reading on the subject
And this seems well written enough for me to follow a long and build it.

1 Like

LOL. I was soooo happy when they let me start using caps, pots, op-amps. Masses, springs, dash pots, were a pita!
Rindert

1 Like

What were you working on?

1 Like

Senior project, vibrations lab, Mech. Engr. Dept. Cal Poly Pomona. Four little rail cars excited by variable speed dc motor w/ adjustable throw crank. LaPlace calcs had to work out. Temperature and humidity had to be adjusted for.
After that, Design of Experiments was just fun.
Rindert

3 Likes

I read that and let out a little scream…

We had a guy that did our vibration stuff for fans.
Very cool how he could tell us things you could barely feel…

I cant do the math now.
But I understand the concept of using Laplace to determine the stability of closed loop feedback systems.
Jeepers batman.
This still makes me want to cry.

We had this big sweaty white haired guy teach us math.
He was from Texas I think, an said he worked at Nasa at one point ( and I have no reason to not believe )

He would scribble, mumble sweat and fill the air with chalk.
Then say something to the effect of how evident it was after your write it all out.
Christ!!!
I still don;t know what he was talking about and I failed that class.
Then there was the instrumentation guy, an Engineer from India.
He had patents on things related to weather monitor stations and his accent was also thick and hard to understand.

Now that I am much older I wish I could do these things, just because i want to understand how things actually work

3 Likes

I also want a couple of Jesse’s auto mixers

5 Likes