For one project I needed a variable voltage source, so I built one.
Disclaimer: this is more like a proof-of-concept; the project I needed this for involved controlling small DC motors, and I wanted to mimic the behavior of a model railroad transformer, instead of using a PWM directly as in PC fan applications. From today's standpoint the linearity of the output seems pretty crude and I'm sure I could do better.
In essence, the variable voltage source feeds a smoothed PWM, generated using a microcontroller, into an emitter follower to increase the voltage stiffness. I actually used a pair of transistors, a BC547 general-purpose transistor as first stage and a BD437 power transistor as output stage. I also added a flyback diode to protect the circuit from voltage spikes when driving inductive loads.
Because of this configuration, the output voltage can never reach the supply voltage, but will always stay about two diode drops below (even though this drastically changes with output load). With loads of 100 ohms or less, the output shows a pretty linear connection between PWM duty cycle and output voltage.
When I created this, I didn't care too much about the precision when driving small loads. I guess the circuit could be improved in terms of removing charge from the transistors' bases and addressing the topic of transistor capacitance.
I used an ATtiny2313 microcontroller, together with a MAX232 to convert UART voltage levels into RS232 voltage levels and back. The initial software was simple and used a text based protocol, with a syntax borrowed from POP3:
+OK voltsrc 1.0 ready
SET A 100
+OK Channel set
SET B 150
+OK Channel set
SET A 0
+OK Channel set
The values being supplied are PWM duty cycles in the range from 0 to 255. A and B address the two different channels in my setup.
A later version (ee_curve) was more elaborated and uses characteristic curves stored in the ATtiny's internal EEPROM. Each channel supports such a curve with 5 interpolation points each, allowing the user to specify an output target voltage in millivolts. Due to the limited amount of program memory, this later version uses a binary protocol and thus requires a special control program on the PC side (instead of an arbitrary terminal program).