Buck converter's output voltage oscillates ±1V around the setpoint (e.g., 20V → 19V-21V) despite PID tuning, and won't stabilize to a constant value

#include <PID_v1.h>
#include <PWM.h>  // For high-frequency PWM
#include <Wire.h>
//#include <LiquidCrystal_PCF8574.h>

// Pin Definitions
#define POT_PIN A0       // Potentiometer input (0–25V scaled down to 0–5V)
#define FEEDBACK_PIN A1  // Output voltage feedback (via voltage divider)
#define PWM_PIN 9        // PWM output pin

// LCD
//LiquidCrystal_PCF8574 lcd(0x27);

// Constants
const int32_t PWM_FREQ = 20000;        // 20kHz PWM frequency
const float VOUT_MAX = 25.0;           // Max output voltage
const float ADC_SCALE = 5.0 / 1024.0;  // Convert ADC to voltage

// PID Variables
double Setpoint, Input, Output;
double lastInput = 0.0;  // For low-pass filter

// PID Tuning Parameters
double aggKp = 2.0, aggKi = 5.0, aggKd = 0.05;
double consKp = 1.0, consKi = 1.0, consKd = 0.01;

PID myPID(&Input, &Output, &Setpoint, aggKp, aggKi, aggKd, DIRECT);

// Timing
unsigned long lastLCDupdate = 0;
const unsigned long LCD_UPDATE_INTERVAL = 200;

double lastVref = 0.0;

void setup() {
  pinMode(PWM_PIN, OUTPUT);
  InitTimersSafe();
  SetPinFrequencySafe(PWM_PIN, PWM_FREQ);

  Serial.begin(9600);
  Wire.begin();
  // lcd.begin(16, 2);
  // lcd.setBacklight(255);
  // lcd.print("Buck Converter");
  // delay(1000);
  // lcd.clear();

  myPID.SetMode(AUTOMATIC);
  myPID.SetSampleTime(200);       // Sample every 100ms
  myPID.SetOutputLimits(0, 255);  // PWM range
}


void loop() {
  // --- Read and filter Vref (A0) ---
  double pot_voltage_raw = analogRead(POT_PIN) * ADC_SCALE;  // 0–5V
  double pot_voltage_filtered = 0.9 * lastVref + 0.1 * pot_voltage_raw;
  lastVref = pot_voltage_filtered;

  Setpoint = pot_voltage_filtered * (VOUT_MAX / 5.0);  // Scale 0–5V to 0–25V

  // --- Read and filter Vout (A1) ---
  double raw_voltage = analogRead(FEEDBACK_PIN) * ADC_SCALE;
  double rawInput = raw_voltage * (VOUT_MAX / 5.0);  // Scale to 0–25V
  Input = 0.9 * lastInput + 0.1 * rawInput;
  lastInput = Input;

  // --- Adaptive PID Tuning ---
  double error = abs(Setpoint - Input);
  if (error > 1.5) {
    myPID.SetTunings(aggKp, aggKi, aggKd);
  } else {
    myPID.SetTunings(consKp, consKi, consKd);
  }

  // --- PID Computation ---
  myPID.Compute();

  // --- Apply PWM Output ---
  int duty_cycle = (int)constrain(Output, 0, 255);
  pwmWrite(PWM_PIN, duty_cycle);

  // --- LCD Update (optional) ---
  // if (millis() - lastLCDupdate >= LCD_UPDATE_INTERVAL) {
  //   lastLCDupdate = millis();

  //   lcd.setCursor(0, 0);
  //   lcd.print("Vref: ");
  //   lcd.print(Setpoint, 1);
  //   lcd.print("V   ");

  //   lcd.setCursor(0, 1);
  //   lcd.print("Vout: ");
  //   lcd.print(Input, 1);
  //   lcd.print("V   ");
  // }

  // --- Serial Debug ---
  Serial.print("Vref: ");
  Serial.print(Setpoint, 2);
  Serial.print(" V | Vout: ");
  Serial.print(Input, 2);
  Serial.print(" V | PWM: ");
  Serial.println(duty_cycle);

  delay(10);  // Small delay
}

I'm working on a closed-loop buck converter using an Arduino Uno for voltage regulation. Despite tuning the PID parameters, my output voltage doesn't settle to a constant value and keeps oscillating around the setpoint (e.g., if setpoint = 20V, output oscillates between 19V–21V).

System Specifications

  1. Hardware:
  • Input Voltage: 25V DC
  • Output Voltage Range: 0–25V (scaled to 0–5V via voltage divider for feedback)
  • Switching Frequency: 20kHz (using PWM.h library)
  • Components:
    • Inductor: 9mH
    • Capacitor: 470μF (output filter)
    • Load: 1Ω resistor (max 2A current)
    • MOSFET: IRFZ44N (with gate driver)
  1. Control Logic:
  • Setpoint: Potentiometer (A0) → Scaled to 0–25V
  • Feedback: Voltage divider (A1) → Scaled to 0–25V
  • PID Library: PID_v1.h
  • Non-blocking code with adaptive tuning:
    • Aggressive PID (error > 1.5V): Kp=2.0, Ki=5.0, Kd=0.05
    • Conservative PID (error ≤ 1.5V): Kp=1.0, Ki=1.0, Kd=0.01
  1. Observed Behavior:
  • Output voltage oscillates ±1V around setpoint (e.g., 20V → 19V–21V).
  • Oscillations persist even after trying multiple PID tunings (see below).

I didn't "study" your code but try taking out the delay and the print statements which also take time. Delays in a feedback loop can cause instability/oscillation.

...I have no idea if the Arduino is fast enough without the delay.

2 Likes

Is it better to use millis() instead of delay() in a PID control loop on Arduino? Also, could you suggest tuning strategies for the PID constants (Kp, Ki, Kd)?

My own experiments with a regulated boost converter showed similar behavior and I fixed it by making the loop run faster. I used the ATtiny44A, so Arduino could be fast enough; Just try to keep a tight loop...

One question: is the behavior different under load? If so, in what way?

Yes, try to get rid of delays (and serial print) and use millis() for timing. I didn't use PID so I can't share experiences there...

Thank you!! Increasing the loop speed makes sense, and I’ll apply the same approach (tight timing, no delays) to my Arduino setup.

For your question about load behavior: I haven’t tested with other loads yet—currently, I’m only using a 48V DC motor (1Ω resistance). If you’ve observed load-specific effects in your projects, I’d love to hear your insights!

I'd really appreciate any additional suggestions or insights you might have!

FYI: I've used my ATtiny44 boost converter to charge a 10-cell NiMH battery from a 12V input. In the loop I check input and output voltages to start/stop charging by switching a mosfet. It regulates the output voltage to be between 12V and 15V while keeping the current in line with the input voltage. That input voltage comes from a solar panel which collapses once current taken is too large...

Once the mosfet is switched on the voltage is very stable. When off it still sways a bit...

1 Like

Just to add my input, if you need serial output for debugging or any other reason, it is better to use higher baud rates to avoid or reduce latency due to serial communications.

I always start with at least 115200 baud, because at 9600 you are transmitting up to 960 bytes per second. Printing these 36 bytes for "serial debug" output theoretically takes 37.5 ms per cycle (ok, it's less than that because of the serial buffer, but serial printing isn't a free lunch :wink: ).

If you need the highest code speed possible, it is recommended to avoid serial output when not needed (for example, you can remove the instruction from the final "production" version using the "#define DEBUG" and "#ifdef DEBUG" directives).
Alternatively, I suggest changing the serial baud rate to 115200 or higher to speed up the output, and printing the serial debug at a slower rate (for example, only if Vout changes from the previous cycle, or 2 times per second...).

1 Like

Thank you for your inputs, much appreciated :slight_smile:

Hello,

I am brand new to the group and to micro controllers, just purchased a couple of UNOs and I have no experience with a digital power supply, just analog but have a few thoughts on this:

  1. Is it possible that the exact PWM width needed for 20V is bewteen two values? For example for a 10 bit register with values from 0-1023 you may need 964.5 so the loop bounces back and forth between 964 and 965. Is the oscillation the same for all set points?

  2. I can't follow your code yet, is the reference external? Did you look at it on the scope?

  3. Did you try running it open loop at some fixed duty cycle to verify there is no oscillation?

  4. For an analog control loop I would start with a Type 1 compensator with a large cap, what is the equivalent for a digital loop? The transient response would be terrible but if the input voltage is clean and the load is fixed it should regulate well at DC. Did you look at the input voltage on the scope?

-kooner

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.