Building an arduino bench power supply

I've always wanted to know how a bench power supply works. It's always been a mystery to me what happens in between 230V AC going in one one side and 3 to 15V DC popping out at the other — and especially, how the thing survives being shorted so painlessly. I've also always wondered how my digital multimeter works. So when I was looking for something to do with my Arduino recently, I had the idea of building a power supply that also takes measurements and reports on the current and maybe other things.

I'm surprised by the amount of knowledge that can be gained through such a seemingly simple project.

Starting off

First of all, when I asked my dad how the power supply works that he built ages ago, he showed me this schematic:

/files/netzteil_schaltplan_dt0001.jpg

(The image file's creation date is in February, so I've been working on this thing for a while...)

This schematic all by itself is already quite a lot to comprehend if you have no idea how it all plays together. I still haven't grasped it completely, but the LM-741 op amp is the key component: It gets a voltage signal on port 3 and regulates its output on port 6 in such a way that the feedback voltage on port 2 matches that on port 3. That way, the voltage at the output of the power supply stays the same, regardless of how much current is being drawn, because the op amp turns up the power if there's a hungry load attached.

The output of the op amp is then simply attached to a small transistor that fires up a set of power transistors, where the output voltage comes from.

But where does the 741 get its input signal from?

Pretty Weird Machinery (PWM)

In early experiments, I tried feeding the 741 using PWM. This didn't really work out because at the time, I had no idea how to smoothen the current, so I just tried putting random capacitors in. But since I didn't know I had to add a resistor in front of the caps, the power simply stayed at 5V then. (I guess I succeeded in smoothing it, at least.)

So I decided to add a digital-analog converter. Since I'd always wanted to do something with I²C and the Arduino supports it, I searched for an I²C-controllable DAC and found the PCF8591. It has three inputs and one output. I connected the output to the 741's input port, and by setting its output to different values, I could control the output voltage. So far, so good.

Measurements

Measuring voltages is pretty much trivial with an ADC: You just attach it to a port and the ADC does the rest.

I then read that one can measure a current by putting a 1Ω resistor into the circuit and measuring the voltages on both sides of the resistor. Then you calculate the current from the voltages using Ohm's law, \(R = \frac{U}{I} \Rightarrow I = \frac{U2 - U1}{R}\).

Since the ADC runs at 5 Volts, but the power supply is supposed to support higher voltages, I can't feed them into the ADC directly. Instead, I have to reduce the voltages using a voltage divider. I have no idea how I got to these values, but I used 10kΩ and 5kΩ resistors (the latter being two 10kΩ resistors hooked up in parallel). Using these, when the input voltage is 15V, the ADC is fed 5V. I added such voltage dividers in three places: One directly after the rectifier to measure the input voltage and one on each side of the shunt. I hooked those up to the ADC.

All in all, this part went pretty smoothly. It took me a couple days to figure everything out, but this was kinda straight-forward. Getting the Arduino to actually make some sense out of the values it is seeing was a whole nuther story, because I just couldn't get the Arduino and my multimeter to agree on the voltages they were seeing. Since measuring didn't work, I didn't even try to implement sending in a voltage and watching the Arduino adjust the power supply to deliver exactly that voltage.

Back to basics

The main issue I had was that I had no idea how to convert the values from the ADC into an actual voltage, and how the supply voltage of about 17V, the arduino's VCC of about 5V, the actual output voltage of the power supply and the voltage I'd like the thing to produce would interact: Does it make a difference what input voltage I have? Which value do I have to feed into the OpAmp in order to get a specific output voltage? Where should the feedback for the OpAmp come from? How do I dimension the voltage dividers?

So far, I had always tried taking a couple measurements and then writing a linear equation (\(y = ax + b\)) to turn "ADC units" into Volts, but I always ended up with values that were a little bit off (2-5 Volts), which can become frustrating after a while.

Through moar googling, I found Building an Arduino DC Voltmeter, which is exactly the part of my setup that refused to work correctly — and they even explain the maths behind it!

The formula for calculating values in a potential divider is:

Vout = (R2 / (R1 + R2)) * Vin

If the divider for the Arduino voltmeter is functioning correctly then Vout will be a maximum of 5V, and so you can calculate the maximum input voltage to the circuit:

Vmax = 5.0 / (R2 / (R1 + R2))

I had completely ignored the fact that I'm actually using a two-step process for measuring: Dividing the input voltage into something smaller and then measuring this smaller voltage as a fraction of some kind of reference voltage. I had tried to find a single formula that handles both of these steps at once. Also I had completely ignored the fact that I have to have a steady reference voltage for all this stuff to work correctly. So I decided to do the calculations once manually step by step, before trying to turn them into code.

Digging through the formulas

Let's say we have a supply voltage of 12V AC, and we want to get 12V DC out. The rectification turns those 12V AC into 16.97V DC (it gets multiplied by \(\sqrt{2}\)).

  1. What input value do I need to feed into the OpAmp?

    \(12 / 16.97 \approx 0.7\), so we'd have to fire the transistors up to 70% — but the (-) port of the OpAmp is hardwired to a voltage divider made for 30V, so it will always get the feedback for 30V, no matter the voltage the power supply is currently operating on. \(12 / 30 \approx 0.4\), so the OpAmp's input has to be around 40%.

    So, no matter the voltage the power supply is currently operating on:

    1. I'll always set the DAC as if we were on 30V.

    2. the ADC will always return values as if we were on 30V.

  2. How do I measure voltages?

    For the above formulas, we let R1 be 50k and R2 be 10k, so that if my input voltage is 30V, I'll get:

    >>> r1 = 50000
    >>> r2 = 10000
    >>> r2 / (r1 + r2) * 30
    5.0
    

    The other way round: If I see 5V, that means the input voltage is:

    >>> 5 / r2 * (r1 + r2)
    30.0
    
  3. The whole thing is measured against a stabilised VCC constantly held at which voltage?

    When the measured voltage equals VCC, the ADC returns 255. Otherweise, \(v_{measured} = v_{max} · \frac{n}{255}\), where n is the value returned from the sensor. So, we need to know \(v_{max}\).

    There are two ways to find out:

    1. Measure it using the multimeter and hardcode it

    2. Add a 1.2V voltage reference, measure its output using the Arduino's analog inputs, and then: \(v_{max} = 1.2V · \frac{1024}{n}\).

    I chose to trust the voltage reference more than I trust the voltage stabilizer used to supply the 5V, so I went ahead and added one.

At that point, I didn't even feel like finishing the example, because I had everything I needed, so I put it in code:

// Voltage measurements are done using a voltage divider to reduce the voltage from
// up to 30V to <=5V. rVCC is the resistor going up to VCC, rGND goes to ground.

unsigned long rvcc = 50000; // Ohms of VCC resistor
unsigned long rgnd = 10000; // Ohms of GND resistor
unsigned long vref =  1200; // mV of reference voltage
unsigned long vcc  =     0; // current VCC (calculated from vref)

unsigned int pinref = A3;   // pin to read vref from

unsigned long toVolts(unsigned long x){
    unsigned long vx = vcc * x / 255; // Convert to measured volts
    return vx * (rvcc + rgnd) / rgnd; // Convert to actual volts
}

void loop(){
    // Among other stuff...

    long valref = analogRead(pinref);
    vcc = vref * 1024 / valref;

    Wire.requestFrom(i2caddr, 1);
    Serial.print( toVolts( Wire.read() ) );
}

I also chose to calculate everything in mV and mA, moving from floats to ints; and finally, my measurements started making sense!

Adding user input

Reversing the formulas to process user input was also a bit hard because I started off by wanting to compare to some hardcoded maximum voltage, repeating mistakes from the past, and thereby calculating complete nonsense. The correct way to do it is strictly reversing the operations we do above:

unsigned int toLevel(unsigned long vin){
    unsigned long vx = vin * rgnd / (rvcc + rgnd); // Convert to measured volts
    if( vx > vcc ){
        // Can't output more than 100%
        return 255;
    }
    return vx * 255 / vcc;                         // Convert to level
}

void loop(){
    level = toLevel( Serial.parseInt() );

    // Send new level over I²C
    Wire.beginTransmission(i2caddr);
    Wire.write(0x40);
    Wire.write(level);
    Wire.endTransmission();
}

Now I can send a voltage to the Arduino encoded as "7400" for 7.4V, and the power supply will spit out 7.4V. :)