This project started as a way to create an universal digitally controlled switch mode power supply module base on the Arduino development platform. As I worked on the design, the platform turn out to be an excellent bi-directional DC-DC converter that has many uses.

As the control system is based on a micro controller, all the parameters can be adjusted though changes in the firmware. The basic control loop use a simple integrate operation. This is the I in the PID controller. It is fairly straight forward to add the P and D operation to the control loop, though there should not be any need to add the differential part.

I picked Arduino IDE as the platform as it is very popular in the maker community and is very easy to use.

What makes this design different than the usual DC-DC converter is this design let the power flow in either directions. The current can flow from the low voltage side to high voltage side and vice versa. By changing the firmware used, the converter can either regulate on the input voltage, output voltage, input current or output current. The project is an excellent platform to learn how switching mode power supply works.

## Step 1: Switch Mode Power Supply Basics

There are two basic type of switch mode power supply, buck mode and boost mode. For buck mode converter, the output voltage is always equal or less than input voltage. As for boost mode converter, the output voltage is always equal or greater than input voltage. Both type of power converters contain a transistor, a diode, an inductor and a capacitor.

## Step 2: Direction-less Power Converter

By using a half-bridge design, it is possible to create a power converter that allows energy to flow in either directions. Compare to bulk or boost type converter, it simply replaces the diode with another transistor. In forward operation, the two transistor chop the high input voltage by the PWM ratio and the LC filter remove the high frequency component to produce the output voltage.

In the reverse operation, the current from the low voltage input flow to the ground though the inductor and the low side transistor. When the high side transistor turns on and the low side turns off, the inductor current is directed to the output on the high voltage side. In this design, the current in the inductor is always flowing, unlike the BULK or BOOST converter design, which the current can goes down to zero when the load is light.

## Step 3: The Design

The design center around the ATMEGA328 micro controller. Timer 1 is used to generate the PWM signal that drives a set of MOSFET. The high side driver is the IRF5305 P-channel MOSFET while the low side driver is the IRFZ44N N-channel MOSFET. Two VN2222 are used as a level shifter to drive the MOSFETs with the high gate voltage required. A 10V zener diode is used to limit the Vgs of the MOSFET to prevent stressing the gate.

The current sensor build with a series 0.1ohm resistor and a 10x amplifier build around MCP602. The current sensor senses current flow on the ground line. The current sensor is bi-directional with range up to 2.5A. The bi-directional current sensor can be used to measure current going in or going out of the power converter. If higher operation current is desired, a smaller sensing resistor can be used.

Note, there is a typo in the schematic. The micro controller should be labeled as ATMEGA328.

A PDF version of the schematic is included here, note the different package (28MLF v.s. 28PDIP) of the ATMEGA328 used in the design.

## Step 4: Firmware Example - Bulk Mode With Output Regulation

/* * Half bridge mode SW bulk/boost controller control routine * */ #define PWM_OUT_PIN_H 10 // PIN16 - OC1B #define PWM_OUT_PIN_L 9 // PIN15 - OC1A #define LED_PIN 11 // PIN17 // Setup Timer 0 for PWM mode, two channels void setup_tmr1_pwm(void) { // Enable the port, set for PWM/dual slope mode TCCR1A = (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0) | (1 << WGM10) | (0 << WGM11) ; TCCR1B = (0 << WGM12) | (0 << WGM13) | // Use fix 0xff for top (0 << CS11) | (1 << CS10); // clkIO = /1 // Phase correct, 0xff for top } void inline set_tmr1_pwm(uint8_t ch0, uint8_t ch1) { OCR1AL = ch0; // LOW OCR1BL = ch1; // HIGH } // the setup routine runs once when you press reset: void setup() { // initialize serial communication at 9600 bits per second: Serial.begin(115200); // Not activate high side yet pinMode(PWM_OUT_PIN_H, OUTPUT); digitalWrite(PWM_OUT_PIN_H, 0); // PWM the low side pinMode(PWM_OUT_PIN_L, OUTPUT); setup_tmr1_pwm(); pinMode(LED_PIN, OUTPUT); // Use TIMER0 compare output to drive the mS interrupt OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); } int pwmValue = 0; int Integrate = 0; int in_Integrate = 0; int g_outVValue; int g_outIValue; int g_inVValue; // 1mS loop, do not use delay... SIGNAL(TIMER0_COMPA_vect) { digitalWrite(LED_PIN, HIGH); g_inVValue = analogRead(A0); g_outVValue = analogRead(A1); g_outIValue = analogRead(A2); // Analog reading (0 - 1023), resistor divider ratio of 4.3x float voltage = g_outVValue * (4.3 * 5.0 / 1023.0); float in_vol = g_inVValue * (4.3 * 5.0 / 1023.0); // Compute difference, hard code the set point // Low voltage is 5.0V // High voltage is 20.0V float diff = 5.0 - voltage; float in_diff = in_vol - 20.0; // Integrator, integrate both input and output voltage separately Integrate += diff * 16; in_Integrate += in_diff * 16; // Saturation limit of the integrator, at least 255*16 if (Integrate > 10000) Integrate = 10000; if (Integrate < -10000) Integrate = -10000; if (in_Integrate > 10000) in_Integrate = 10000; if (in_Integrate < -10000) in_Integrate = -10000; // Use the greater of either two // Regulate either low side of no less than 5V // or High side of no more than 20V pwmValue = Integrate / 16; // PWM value limitation if (pwmValue > 255) pwmValue = 255; if (pwmValue < 0) pwmValue = 0; set_tmr1_pwm((pwmValue + 5) > 255 ? 255 : (pwmValue + 5), pwmValue); digitalWrite(LED_PIN, LOW); } // the loop routine runs over and over again forever: void loop() { // input on analog pin 0: Supply Voltage // input on analog pin 1: Feedback line voltage // input on analog pin 2: Current sensor voltage /* if (Serial.available() > 0) { int inch = Serial.read(); if (inch == 'u') pwmValue++; if (inch == 'd') pwmValue--; if (pwmValue < 0) pwmValue = 0; if (pwmValue > 255) pwmValue = 255; }*/ //Serial.println(g_outIValue); Serial.println(Integrate/16); Serial.println(pwmValue); delay(1000); }

Basic bulk converter control logic. The output voltage is preset to 5.0V in the firmware. This is the basic starting point for building a simple power supply.

The control loop runs in 1mS timer interrupt. TMR0 is used for interrupt generation. TMR1 is used for PWM generation. The resolution of the PWM is 8bit.

## Step 5: Firmware Example - Boost Mode With Low Voltage Limit

/* * * Half bridge mode SW bulk/boost controller control routine * */ #define PWM_OUT_PIN_H 10 // PIN16 - OC1B #define PWM_OUT_PIN_L 9 // PIN15 - OC1A #define LED_PIN 11 // PIN17 // Setup Timer 0 for PWM mode, two channels void setup_tmr1_pwm(void) { // Enable the port, set for PWM/dual slope mode TCCR1A = (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0) | (1 << WGM10) | (0 << WGM11) ; TCCR1B = (0 << WGM12) | (0 << WGM13) | // Use fix 0xff for top (0 << CS11) | (1 << CS10); // clkIO = /1 // Phase correct, 0xff for top } void inline set_tmr1_pwm(uint8_t ch0, uint8_t ch1) { OCR1AL = ch0; // LOW OCR1BL = ch1; // HIGH } // the setup routine runs once when you press reset: void setup() { // initialize serial communication at 9600 bits per second: Serial.begin(115200); // Not activate high side yet pinMode(PWM_OUT_PIN_H, OUTPUT); digitalWrite(PWM_OUT_PIN_H, 0); // PWM the low side pinMode(PWM_OUT_PIN_L, OUTPUT); setup_tmr1_pwm(); pinMode(LED_PIN, OUTPUT); // Use TIMER0 compare output to drive the mS interrupt OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); } int pwmValue = 0; int Integrate = 0; int in_Integrate = 0; int g_outVValue; int g_outIValue; int g_inVValue; // 1mS loop, do not use delay... SIGNAL(TIMER0_COMPA_vect) { digitalWrite(LED_PIN, HIGH); g_inVValue = analogRead(A0); g_outVValue = analogRead(A1); g_outIValue = analogRead(A2); // Analog reading (0 - 1023), resistor divider ratio of 4.3x float voltage = g_outVValue * (4.3 * 5.0 / 1023.0); float in_vol = g_inVValue * (4.3 * 5.0 / 1023.0); // Compute difference, hard code the set point // Low voltage is 5.0V // High voltage is 20.0V float diff = 5.0 - voltage; float in_diff = in_vol - 20.0; // Integrator, integrate both input and output voltage separately Integrate += diff * 16; in_Integrate += in_diff * 16; // Saturation limit of the integrator, at least 255*16 if (Integrate > 10000) Integrate = 10000; if (Integrate < -10000) Integrate = -10000; if (in_Integrate > 10000) in_Integrate = 10000; if (in_Integrate < -10000) in_Integrate = -10000; // Use the greater of either two // Regulate either low side of no less than 5V // or High side of no more than 20V if (Integrate > in_Integrate) pwmValue = Integrate / 16; else pwmValue = in_Integrate / 16; <p>// PWM value limitation<br> if (pwmValue > 255) pwmValue = 255; if (pwmValue < 0) pwmValue = 0; set_tmr1_pwm((pwmValue + 5) > 255 ? 255 : (pwmValue + 5), pwmValue); digitalWrite(LED_PIN, LOW); }</p><p>// the loop routine runs over and over again forever: void loop() { // input on analog pin 0: Supply Voltage // input on analog pin 1: Feedback line voltage // input on analog pin 2: Current sensor voltage /* if (Serial.available() > 0) { int inch = Serial.read(); if (inch == 'u') pwmValue++; if (inch == 'd') pwmValue--; if (pwmValue < 0) pwmValue = 0; if (pwmValue > 255) pwmValue = 255; }*/ //Serial.println(g_outIValue); Serial.println(Integrate/16); Serial.println(pwmValue); delay(1000); }</p>

This control loop regulate the high voltage side to be no more than 20V and low voltage side to be no less than 5V. There are number of possible use for this control logic. One is to run the converter is boost mode with input voltage higher than 5V and output voltage at 20V. The other use is to have the input voltage to be held at 20V no matter the input current, which is commonly utilized for designing a maximum power point tracker for solar panel battery charger.

Warning when experimenting with bootst mode. Make sure you test with current limiting power supply. The switching elements used in this design have very low impedance and is capable of driving quite a bit of current when the control loop goes out of control, which happens easily because the control loop isn't optimized. Large current can potentially damage the switching elements unintentionally.

## Step 6: Advance Example, Add Current Limiting and Low Voltage Cutoff

Example control loop:

SIGNAL(TIMER0_COMPA_vect)

{

digitalWrite(LED_PIN, HIGH);

g_inVValue = analogRead(A0);

g_outVValue = analogRead(A1);

g_outIValue = analogRead(A2);

// Analog reading (0 - 1023), resistor divider ratio of 4.3x

float voltage = g_outVValue * (4.3 * 5.0 / 1023.0);

float in_vol = g_inVValue * (4.3 * 5.0 / 1023.0);

float amp = 2.5 - g_outIValue * (5.0 / 1023.0);

// Compute difference, hard code the set point

// Low voltage is 5.0V

// High voltage is 20.0V

float diff = 14.0 - voltage; // Low side battery set point

//float in_diff = in_vol - 20.0; // High side battery max limit

float amp_diff = ((float)g_setPoint * 0.1) - amp; // Current limit

debugf = amp;

// Integrator, integrate both input and output voltage separately

Integrate += diff * 16 * 1;

//in_Integrate += in_diff * 16 * 16;

amp_Integrate += amp_diff * 16;

// Saturation limit of the integrator, at least 255*16

if (Integrate > 4096) Integrate = 4096;

if (Integrate < -4096) Integrate = -4096;

//if (in_Integrate > 4096) in_Integrate = 4096;

//if (in_Integrate < -4096) in_Integrate = -4096;

if (amp_Integrate > 4096) amp_Integrate = 4096;

if (amp_Integrate < -4096) amp_Integrate = -4096;

// Bulk converter -

// Start with pwm value set to feedback integrate value

pwmValue = Integrate;

if (Integrate > amp_Integrate)

pwmValue = amp_Integrate;

pwmValue /= 16;

// Set minimal input voltage to 12V

// Minimal input voltage before turn on

if (in_vol < 12.0) {

Integrate = -4096; // Soft start

set_tmr1_pwm_off();

bPWMoff = 1;

}

else {

if (bPWMoff) {

bPWMoff = 0;

setup_tmr1_pwm();

}

}

// PWM value limitation

if (pwmValue > 255) pwmValue = 255;

if (pwmValue < 0) pwmValue = 0; // Boost mode, never go down to 0

set_tmr1_pwm((pwmValue + 5) > 255 ? 255 : (pwmValue + 5), pwmValue);

digitalWrite(LED_PIN, LOW);

}

Please refer to the attached file for the full code:

This change add an adjustable current output, suitable for battery charging. The current can be changed though the serial terminal using 'O' and 'L' key. The output voltage is fixed to 14V in the firmware. This makes a simple battery charger possible.

## Step 7: End Notes

There are plenty of improvements that can be done with the project. Experienced micro controller programmer will notice the use of floating point calculation in the feedback control logic. This can certainly be replaced with fixed point math. I left the floating point math in place for easy understanding of the control logic.

The integration gain is set pretty low right now, causing slow tracking and excessive overshoot. This can certainly be improved by increasing the gain. The control loop sample frequency is also pretty slow at 1mS, this can be increased easily by 10x by utilizing TMR2 as the source of the interrupt instead of TMR0.

The example firmwares here are simple starting points for experimentation. There is really no limit as to how the control logic can be improved other than running of memory or time...

Have fun hacking...

## Discussions