Arduino Electronic Boost Controller

Hello guys, I’m trying to build a PID boost controller for turbocharged cars. The code works as it should, but the pressure oscilates around the target pressure more than I’d want it and I’m afraid it’s not because of not-good-enough values of P,I and D (sTune library sets the values automatically and I have also tried setting them myself with the best results being close to the sTune results.)

The picture below is from the code I attached below but the blue line oscilates around the 180 kPa target by more than ±10 kPa. An acceptable oscilation would be ± 10 kPa or less

Hardware:

  • Arduino UNO
  • MAC valve DDBA 12Volt DC (35A-AAA-DDBA-1BA)
  • MPX 5500DP pressure sensor
  • 16x2 LCD screen
  • 1N4001 diode
  • IRFZ44 Mosfet
  • LM2596 Buck converter
  • cables, resistors, a button, a capacitor to filter the sensor’s input…and that’s all pretty much

Code:


```cpp
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <PID_v1.h>
#include "sTune.h"
#include <math.h>

// ================== USER SETTINGS ==================
static const double setpoint_kPa_init = 180.0;  // target boost
static double setpoint_kPa = setpoint_kPa_init;

// Air source (battery inflator) soft limit (it cuts ~250 kPa)
static const double SUPPLY_MAX_KPA = 245.0;   // safety headroom for guard
static const double OVERSHOOT_MARGIN = 10.0;  // start backing off above SP + margin

// PID gains for TRIM (15 ms sample time, feed-forward enabled)
static double Kp = 2.2, Ki = 0.34, Kd = 0.32;

// Feed-forward base and trim ranges
static const double BASE_DUTY_FRAC = 0.58;   // 60% base duty
static const double MAX_BASE_BACKOFF = 0.1;  // up to -10% base when guarding
static const double TRIM_FRAC = 0.14;        // PID trims ±14% around base

// Keep the valve in its linear band (final duty clamp)
static const uint8_t DUTY_CLAMP_MIN = 102;  // 40% of 255
static const uint8_t DUTY_CLAMP_MAX = 204;  // 80% of 255

// Pressure filtering
static const uint8_t FILTER_SAMPLES = 5;
static const unsigned FILTER_US_DELAY = 300;  // µs between ADC reads

// Integral zone (don’t integrate when far from target)
static const double I_ZONE_KPA = 15.0;

// Logging / LCD periods
static const unsigned LOG_MS = 80;   // ~12.5 Hz serial log
static const unsigned LCD_MS = 500;  // 2 Hz LCD
// =====================================================

LiquidCrystal_I2C lcd(0x3F, 16, 2);

const int pressurePin = A0;  // MPX sensor
const int solenoidPin = 11;  // OC2A (Timer2 PWM @ ~31 Hz)
const int buttonPin = 7;     // Autotune trigger button (to GND)

double PV_kPa, OUT_trim;
PID boostPID(&PV_kPa, &OUT_trim, &setpoint_kPa, Kp, Ki, Kd, DIRECT);

unsigned long lastLCD = 0, lastLog = 0;
uint8_t lastDuty = 0;
bool buttonPrevPressed = false;

// ---------- sTune integration ----------
enum Mode : uint8_t { MODE_NORMAL = 0,
                      MODE_AUTOTUNE = 1,
                      MODE_APPLY = 2 };
static Mode runMode = MODE_NORMAL;

// sTune input/output variables
static float tuneInput = 0.0f;   // mirrors PV
static float tuneOutput = 0.0f;  // raw duty during tuning

// construct tuner ONCE globally (no assignment!)
sTune tuner(&tuneInput, &tuneOutput,
            sTune::DampedOsc_PID,
            sTune::directIP,
            sTune::printSUMMARY);

// plant setup constants
static const float STUNE_INPUT_SPAN = 500.0f;    // kPa full span (MPX5500)
static const float STUNE_OUTPUT_SPAN = 255.0f;   // PWM 8-bit span
static const float STUNE_OUTPUT_START = 130.0f;  // ~51%
static const float STUNE_OUTPUT_STEP = 165.0f;   // ~65%
static const uint32_t STUNE_TEST_SEC = 3;
static const uint32_t STUNE_SETTLE_SEC = 1;
static const uint16_t STUNE_SAMPLES = 250;

// ---------- simple serial command parser ----------
static char _lineBuf[64];
static uint8_t _lineLen = 0;

void printHelp() {
  Serial.println(F("Cmds:"));
  Serial.println(F("  P=#  I=#  D=#          (set individual gains)"));
  Serial.println(F("  PID kp ki kd           (set all three)"));
  Serial.println(F("  GET                    (print current PID gains)"));
  Serial.println(F("  ATSTART                (start sTune autotune)"));
  Serial.println(F("  ATSTOP                 (abort autotune and return to normal)"));
  Serial.println(F("  ATAPPLY                (re-apply last found sTune gains)"));
  Serial.println(F("  SET TGT=###            (set setpoint kPa)"));
}

void handleSerial() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\r') continue;
    if (c == '\n') {
      _lineBuf[_lineLen] = '\0';
      if (_lineLen > 0) {
        char* s = _lineBuf;
        while (*s == ' ') s++;

        if ((s[0] == 'P' || s[0] == 'p') && s[1] == '=') {
          double v = atof(s + 2);
          Kp = v;
          boostPID.SetTunings(Kp, Ki, Kd);
          Serial.print(F("Kp="));
          Serial.println(Kp, 6);
        } else if ((s[0] == 'I' || s[0] == 'i') && s[1] == '=') {
          double v = atof(s + 2);
          Ki = v;
          boostPID.SetTunings(Kp, Ki, Kd);
          Serial.print(F("Ki="));
          Serial.println(Ki, 6);
        } else if ((s[0] == 'D' || s[0] == 'd') && s[1] == '=') {
          double v = atof(s + 2);
          Kd = v;
          boostPID.SetTunings(Kp, Ki, Kd);
          Serial.print(F("Kd="));
          Serial.println(Kd, 6);
        } else if ((s[0] == 'P' || s[0] == 'p') && (s[1] == 'I' || s[1] == 'i') && (s[2] == 'D' || s[2] == 'd')) {
          double nkp = Kp, nki = Ki, nkd = Kd;
          char* p = s + 3;
          while (*p == ' ') p++;
          if (*p) { nkp = atof(p); }
          while (*p && *p != ' ') p++;
          while (*p == ' ') p++;
          if (*p) { nki = atof(p); }
          while (*p && *p != ' ') p++;
          while (*p == ' ') p++;
          if (*p) { nkd = atof(p); }
          Kp = nkp;
          Ki = nki;
          Kd = nkd;
          boostPID.SetTunings(Kp, Ki, Kd);
          Serial.print(F("PID set -> Kp="));
          Serial.print(Kp, 6);
          Serial.print(F(", Ki="));
          Serial.print(Ki, 6);
          Serial.print(F(", Kd="));
          Serial.println(Kd, 6);
        } else if (strncasecmp(s, "GET", 3) == 0) {
          Serial.print(F("Kp="));
          Serial.print(Kp, 6);
          Serial.print(F(" Ki="));
          Serial.print(Ki, 6);
          Serial.print(F(" Kd="));
          Serial.println(Kd, 6);
        } else if (strncasecmp(s, "SET TGT=", 8) == 0) {
          double v = atof(s + 8);
          setpoint_kPa = v;
          Serial.print(F("Setpoint_kPa="));
          Serial.println(setpoint_kPa, 1);
        } else if (strncasecmp(s, "ATSTART", 7) == 0) {
          startAutotune();
        } else if (strncasecmp(s, "ATSTOP", 6) == 0) {
          abortAutotune();
        } else if (strncasecmp(s, "ATAPPLY", 7) == 0) {
          applyLastTunings(true);
        } else {
          printHelp();
        }
      }
      _lineLen = 0;
    } else {
      if (_lineLen < sizeof(_lineBuf) - 1) _lineBuf[_lineLen++] = c;
    }
  }
}

// ---------- Pressure read ----------
float getFilteredPressure_kPa() {
  long sum = 0;
  for (uint8_t i = 0; i < FILTER_SAMPLES; i++) {
    sum += analogRead(pressurePin);
    delayMicroseconds(FILTER_US_DELAY);
  }
  const float avg = sum / (float)FILTER_SAMPLES;

  const float voltage = avg * (5.0f / 1023.0f);
  const float V0 = 0.20f;
  const float spanV = 4.70f;
  float kPa = (voltage - V0) * (500.0f / spanV);

  if (kPa < -50) kPa = -50;
  if (kPa > 550) kPa = 550;
  return kPa;
}

// ---------- Timer2 ~31 Hz PWM ----------
void setupTimer2_31Hz() {
  pinMode(solenoidPin, OUTPUT);
  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2A |= _BV(WGM20);
  TCCR2A |= _BV(COM2A1);
  TCCR2B |= _BV(CS22) | _BV(CS21) | _BV(CS20);
  OCR2A = 0;
}
inline void pwmWrite31Hz(uint8_t duty) {
  OCR2A = duty;
}

// ---------- sTune helpers ----------
void startAutotune() {
  if (runMode == MODE_AUTOTUNE) {
    Serial.println(F("Autotune already running."));
    return;
  }

  tuneInput = (float)PV_kPa;
  tuneOutput = STUNE_OUTPUT_START;

  tuner.Configure(STUNE_INPUT_SPAN, STUNE_OUTPUT_SPAN,
                  STUNE_OUTPUT_START, STUNE_OUTPUT_STEP,
                  STUNE_TEST_SEC, STUNE_SETTLE_SEC, STUNE_SAMPLES);

  tuner.SetEmergencyStop((float)SUPPLY_MAX_KPA);

  runMode = MODE_AUTOTUNE;
  boostPID.SetMode(MANUAL);
  Serial.println(F("\n=== sTune: starting autotune (open-loop step test) ==="));
}

void abortAutotune() {
  if (runMode != MODE_AUTOTUNE) {
    Serial.println(F("Autotune not running."));
    return;
  }
  runMode = MODE_NORMAL;
  boostPID.SetMode(AUTOMATIC);
  Serial.println(F("sTune: aborted. Back to normal PID."));
}

void applyLastTunings(bool announce) {
  float kp_s, kii, kdd;
  tuner.GetAutoTunings(&kp_s, &kii, &kdd);

  double newKp = kp_s;
  double newKi = kp_s * kii;
  double newKd = (kdd > 0.0f) ? (kp_s / kdd) : 0.0;

  if (isfinite(newKp) && isfinite(newKi) && isfinite(newKd) && newKp > 0) {
    Kp = newKp;
    Ki = newKi;
    Kd = newKd;
    boostPID.SetTunings(Kp, Ki, Kd);
    if (announce) {
      Serial.print(F("sTune applied -> Kp="));
      Serial.print(Kp, 6);
      Serial.print(F(", Ki="));
      Serial.print(Ki, 6);
      Serial.print(F(", Kd="));
      Serial.println(Kd, 6);
    }
  } else {
    Serial.println(F("sTune: invalid tunings; keeping old gains."));
  }
}

// ---------- setup ----------
void setup() {
  setupTimer2_31Hz();
  lcd.init();
  lcd.backlight();
  Serial.begin(115200);

  pinMode(buttonPin, INPUT_PULLUP);  // button to GND, active LOW

  const int TRIM_MAX = (int)(TRIM_FRAC * 255.0 + 0.5);
  boostPID.SetOutputLimits(-TRIM_MAX, +TRIM_MAX);
  boostPID.SetSampleTime(15);
  boostPID.SetMode(AUTOMATIC);

  printHelp();
  Serial.println(F("PID serial ready."));
}

// ---------- loop ----------
void loop() {
  handleSerial();

  PV_kPa = getFilteredPressure_kPa();
  tuneInput = (float)PV_kPa;

  // Read button (active LOW)
  bool buttonNowPressed = (digitalRead(buttonPin) == LOW);

  // On rising edge of button (user presses), start autotune
  if (buttonNowPressed && !buttonPrevPressed && runMode == MODE_NORMAL) {
    startAutotune();
  }
  buttonPrevPressed = buttonNowPressed;

  if (PV_kPa >= SUPPLY_MAX_KPA) {
    pwmWrite31Hz(DUTY_CLAMP_MIN);
  }

  if (runMode == MODE_AUTOTUNE) {
    // Only run autotune while the button is held down
    if (buttonNowPressed) {
      uint8_t duty = (uint8_t)constrain((int)roundf(tuneOutput), DUTY_CLAMP_MIN, DUTY_CLAMP_MAX);
      pwmWrite31Hz(duty);
      lastDuty = duty;

      uint8_t st = tuner.Run();

      if (st == sTune::tunings) {
        applyLastTunings(true);
        runMode = MODE_APPLY;
      }
    } else {
      // Button released during autotune -> abort and return to normal
      abortAutotune();
    }
  } else {
    if (runMode == MODE_APPLY) {
      boostPID.SetMode(AUTOMATIC);
      runMode = MODE_NORMAL;
      Serial.println(F("sTune: switching to NORMAL with new gains."));
    }

    double baseFrac = BASE_DUTY_FRAC;
    double backoff = 0.0;
    if (PV_kPa > setpoint_kPa + OVERSHOOT_MARGIN) {
      backoff += (PV_kPa - (setpoint_kPa + OVERSHOOT_MARGIN)) / 40.0;
    }
    if (PV_kPa > SUPPLY_MAX_KPA - 8.0) {
      backoff += (PV_kPa - (SUPPLY_MAX_KPA - 8.0)) / 16.0;
    }
    if (backoff > MAX_BASE_BACKOFF) backoff = MAX_BASE_BACKOFF;
    baseFrac -= backoff;
    if (baseFrac < 0.40) baseFrac = 0.40;

    double e = setpoint_kPa - PV_kPa;
    double Ki_eff = Ki;
    if ((lastDuty >= DUTY_CLAMP_MAX && e > 0) || (lastDuty <= DUTY_CLAMP_MIN && e < 0)) {
      Ki_eff = 0;
    }
    if (fabs(e) > I_ZONE_KPA) {
      Ki_eff = 0;
    }
    boostPID.SetTunings(Kp, Ki_eff, Kd);
    boostPID.Compute();
    boostPID.SetTunings(Kp, Ki, Kd);

    const int baseDuty = (int)(baseFrac * 255.0 + 0.5);
    int duty = baseDuty + (int)OUT_trim;
    if (duty < DUTY_CLAMP_MIN) duty = DUTY_CLAMP_MIN;
    if (duty > DUTY_CLAMP_MAX) duty = DUTY_CLAMP_MAX;
    pwmWrite31Hz((uint8_t)duty);
    lastDuty = (uint8_t)duty;
  }

  const unsigned long now = millis();
  if (now - lastLog >= LOG_MS) {
    lastLog = now;
    const float dutyPct = lastDuty * 100.0f / 255.0f;
    const float trimPct = OUT_trim * 100.0f / 255.0f;
    Serial.print("P:");
    Serial.print(PV_kPa, 1);
    Serial.print("\tT:");
    Serial.print(setpoint_kPa, 0);
    Serial.print("\tD:");
    Serial.print(dutyPct, 1);
    Serial.print("\tTRIM%:");
    Serial.print(trimPct, 1);
    Serial.print("\tMODE:");
    Serial.print(runMode == MODE_NORMAL ? "N" : (runMode == MODE_AUTOTUNE ? "AT" : "AP"));
    Serial.println();
  }

  if (now - lastLCD >= LCD_MS) {
    lastLCD = now;
    const int dutyPctRounded = (int)(lastDuty * 100.0f / 255.0f + 0.5f);

    lcd.setCursor(0, 0);
    lcd.print("P:        kPa ");
    lcd.setCursor(3, 0);
    lcd.print(PV_kPa, 1);

    lcd.setCursor(0, 1);
    if (runMode == MODE_AUTOTUNE) {
      lcd.print("AT RUN  D:");
      lcd.setCursor(10, 1);
      lcd.print(dutyPctRounded);
    } else {
      lcd.print("T:      D:   ");
      lcd.setCursor(3, 1);
      lcd.print(setpoint_kPa, 0);
      lcd.setCursor(10, 1);
      lcd.print(dutyPctRounded);
    }
  }
}

```

So, do you think it might be because I’m using a breadboard instead of soldered wires ? Or something with the frequency of the solenoid valve ? What do you suggest trying in order to reduce the oscilation range ?
Thanks in advance guys !

Make all the repetitive serial.Print() statements into comments and see what changes.

Can You debug print the input data to the PID, the sensor values? Here we say: Shit in, shit out. Jerky sensor data will make trouble for every control strategy.

Nothing really changed. Watching the LCD screen, the readings were same.

Do you mean show the values as the pressure rises ? Or while “idle” ?

During the time when the fluctuations appear. Electrical things can affect sensors to deliver erratic data disturbing the rest of the logic.

Now that I think about it, what you ask is shown in the first picture (not the 0-1023 values but the translated-to-pressure readings). I also remind that the fluctuations happen because the solenoid valve pulses on and off. Our problem is that it fluctuates more than we want it to. Shall I still show the sensor readings in values 0-1023 ?

IS that the problem or is it the time taken to move from on to off? Do all those valves take the same time to do the move?

If the frequency of the solidnoid valve was higher it might work better, indeed. I’ll search whether all these valves use the same frequency, but these MAC valves are used in boost controllers so…

Reading Your post it’s hard to know what those parameters do to the control chain, what importance those data have in the chain of logic.

My idea is to question each sensor.

Down in the last post You have made an observation that during the time solenoids are switched On and/or Off might be interesting.

Do the solenoids have kick back diodes “chewing up” the elctromagnetic “kick back” when they are switched off?

Yes, there is a diode to prevent “kick back“.

Does your software make the assumption that the valves move instantaneously?

Good! How about power supply and current consumption? Risk for any overload there?

No, I don’t explicitly assume the valve is “instantaneous”, but I also don’t model its dynamics analytically. I treat the whole thing “air pump + valve + lines + sensor” as one black-box plant and let the autotune + PID gains absorb whatever delay the hardware actually has.
Some extra information:

  • I clamp the duty into a “linear-ish” region:

    DUTY_CLAMP_MIN = 102;  // ~40%
    DUTY_CLAMP_MAX = 204;  // ~80%
    
    

    and use an I-zone and anti-windup logic. That’s specifically because the valve is not ideal and I don’t want the controller fighting in the fully closed / fully open extremes where the behavior gets non-linear.

  • The control loop runs slower than the PWM:

    boostPID.SetSampleTime(15);  // 15 ms
    
    

    plus I low-pass the pressure (multiple ADC samples, small delay). So the controller is looking at relatively slow pressure changes, not at every tiny instant the valve moves.

Ok, that seems well covered. Is there any mechanical linkage between the solenoid armature and the actual valve?

Everything is inside the solenoid valve. You can see how it looks like here Screenshot by Lightshot

I haven’t calculated it myself to be honest. The guy I paid to create the schematic and choose the proper hardware said that there is no risk for overload.

It’s Your choice to accept that. I don’t buy it. What do You know about that guy? But, please go on.

Thanks for picture. Looking at the company web site data, I see a that energize time for 24 volt units is 6ms and release time is 2ms. Nothing for the 12 volt product. Lower voltage may mean the times are larger.

So, by design, the open and close are not symmetrical. I wonder if that has any bearing on your oscillations?

I was sure I had your valve in my junk box, but could not find it. I did this afternoon, in a different box and in a different storage space. My identical units were all used in circuit board repair stations. The station used a small air pump to provide vacuum and pressure. Vacuum to hold an IC and pressure to blow it off when soldering to a board, or to the scrap pile. VERY little air volume was used or required.

This is with the solenoid removed and showing the actual air opening.

This is the plunger that covers or uncovers the opening.

This is the solenoid assembly as just removed from the base.

And this is the plunger showing the return spring.

My unit is lubricated with some type of silicon jell which may affect the speed of operation.

What are your requirements for air volume when the valve opens?