10 bit analog output

Although I cant post my whole project, here is a little twist that might help some folks. I needed a pair of 0 - 5 V analog outs with 10 bit precision that could drive a 10k load within 1% of either rail. Even finding a buffer amplifier operating that close to the rail with Zout <100 ohm output is not easy. Before resorting to new supply voltages, an idea struck using only resistors and capacitors.

In the Old Skewl we sometimes used a binary weighted divider to turn binary into analog, like this:

Its an alternative to the R/2R ladder more commonly used. Just stick in four bits and a voltage comes out. An Arduino PWM output can be expanded to 10 bit like this:

Arduino analogWrite using bits[7…0] plus digitalWrite to 2 pins for bit[9] and bit[8] does the trick. Precision resistors for accuracy and/or linearity may be required for your application.

My project turns out to only use the top and bottom quarter of the voltage range, so bits 9 and 8 could be set with a single pin. Optionally, in software, the digital output could be set to Hi-Z and allow the PWM to cover the full voltage range with 8 bits of precision, and enable the digital output only to increase the precision to 10 bits in the top and bottom quarter, but my project doesnt need the middle of the range.

So thats the grand design. An interesting detail was the resistors.

I needed a stiff output so I chose some very low values, to use as much “grunt” as an ATmega pin can safely source or sink. ATmega outputs are not ideal at high currents, which creates increasing errors as the resistors get lower in value. Referring to figs 29-160 and 29-162 in data sheet for the ATmega328P used in the Arduino Uno, one can derive from the slope that the pins have an effective internal series resistance of 20 to 30 ohms over temperature.

The binary divider resistors on the output pins must be reduced by a nominal 25 ohms to compensate for this.

Each channel ended up being this:

I was nicely surprised by the low values possible, only 33 ohms and 150 ohms, (or 58 ohms and 175 ohms with internal resistance) although this did require rather large capacitors in the 2-pole filter needed to get the quiet output required. The 43 ohm divider impedance plus a 47 ohm filter resistor keeps the total under 100 ohms required to drive a 10k load to within 1% of the opposite rail.

But do the math, including the 25 ohms internal, and see that in worst case with the outputs in opposite states the current is just over HALF of what can be carried continuously over the entire temperature range. With 2 pairs both fighting themselves the ATmega is barely warm, about the same as when not doing something this crazy.

You can overstress this is with an external short circuit; set the pins to opposite states and then short the output to which ever rail the PWM pin is stuck at. In this case the digital pin will carry about 1/3 more than its ratings, but at room temperature it could probably do this for years.

Two of these channels stimulate an external device, and both are monitored by normal Arduino 10-bit analog inputs. This closes the loop for accuracy during operation, and allows the project to run an exhaustive self-test at start up, including filter step response. Hopefully upon detection of a shorted output I remembered to leave the pins set to the low-stress state.

This is my first Arduino project and its going well, a tribute to the Arduino team and the IDE. Hope some folks find other uses for my 10 bit analog output.

This is a very interesting approach... I guess you probably can only use it in low frequency scenarios? What's the frequency you are working with?

The freq could be as fast as you can bang the bits, essentially the same freq limits as analogWrite() and the PWM. One day I may learn enough to make a library for this. The real speed limitation is the RC filter, which can be anything you want depending on the PWM freq of analogWrite() and if you need to filter for low ripple.

In my application is was necessary to make the output both strong and quiet, but speed was of little concern. So I ended up following it with a very slow RC filter. I cant discuss the application but it involves a voltage more precise than 8 bits “sneaking up” until an event happens, the measurement of interest being the voltage required to trigger the event. If these do not happen within 10% of a supply rail than its a test failure. Omitting the “middle 50%” saved my application one IO pin for each channel, but in general you’d need 2 digital IO pins to augment a PWM to 10 bits.

On start up I write the 0%, 25%, 75% and 100% levels and verify each using analogRead(). Then I bang between 100% and 0% a coupla times and check the step response of my super slow filter. Soon I will also measure a bandgap reference and compensate for Vcc regulation. If all is well I’m ready to test.

The test pops to 75% and pauses for the filter catches up, then it ramps up slowly to 100%. The slew is low so the slow filter does not hurt. It remembers at what level an event triggers on each channel. After 100% it pops to 25%, pauses, then ramps slowly down to 0% to check the lower event thresholds.

Actually after both channels have tripped their events, my application could abandon the remainder of the ramp and drive on, but speed is not important to me.

Perhaps I oughta make a library or funtion that accepts as arguments one PWM pin plus n digital pins, to define an n+8 bit analog out. Then you just write an n=8 bit number. Assuming you put in appropriate resistors and any filtering you might need, your analog appears.