best solution for 12 analog outputs (5 bits)

My project involves setting 12 voltage levels at 12 analog inputs of a controller.
The required levels are:
0V < L0 < 0.425V < L1 < 1.266V < L2 < 1.961V < L3 < 2.514V < L4 < 3.205V < L5 < 4.027V < L6 < 5V

The analog inputs of the controller use voltage dividers with 15 kOhm to 5V and 15 kOhm to GND.

My requirements:

  • Low cost
  • Small area
  • Pin through
  • Voltage levels are fixed (used for initialization of the controller)
  • Steady state < 1 s

I investigated a solution with 3x TLV5620 DAC to have 12x 8 bits DAC channels, test results:

  • I chose the wrong DAC, this DAC is not able to sink current, so it was not able to reach the levels below L3.
  • I can look for a DAC that can sink more current
  • I can look for a DAC that can source more current and lower the base level by an additional resistor to GND

The DAC’s I can find are:

  • Expensive
  • SMD

So I decided to investigate a different solution:

  • PWM (frequency=490Hz)+ RC filter (eg R=1 kOhm, C=47 uF)

This seems feasible for me. Only the capacitor is a bit big.

I can user a smaller capacitor when using a higher PWM frequency. Therefore I was thinking: what would be the best solution when I want to have 12 PWM outputs with fixed duty cycle at high frequency (e.g. 20 kHz). I only need 32 steps resolution.

Is it possible to pre-program a fixed buffer of 32 bits for each voltage level (L0 to L6) and send the bits, bit by bit to a digital output, by using an interrupt routine?
Is it possible to work with 1 interrupt and write 12 digital outputs each cycle? Is it possible to reach 20 kHz? This is the only task the microcontroller has to do.

You can use a mcu digital-out pin to power a divider. And pick your divider to get the desired output levels.

If you need precision output levels, use the digital-out pins to power a voltage reference and then use the reference to power a divider. This approach requires multiple voltage references.

An alternative is to use 1 voltage reference and then pull a divider to ground with the digital-out pin.

Each approach can be made to work.

So I decided to investigate a different solution:

  • PWM (frequency=490Hz)+ RC filter (eg R=1 kOhm, C=47 uF)

This seems feasible for me. Only the capacitor is a bit big.

I can user a smaller capacitor when using a higher PWM frequency.

All you need is the same time constant so R=1 kOhm, C=47 uF is the same as R=10 kOhm, C=4.7 uF or R=100 kOhm, C=0.47 uF.
What will change is the output impedance of the filter, but if you are using a buffer then that will be no problem.

However the TLV5620 have a voltage output so it is a simple matter to arrange some sort of buffer if the output impedance is too high for your application.

Also, it will take some time for the PWM to charge the filter and get the value to settle. How quickly do you need it to respond? Any time you talk about Kohm resistors and uF, you can expect to wait a while!

How about a couple of these? $1.89 each
http://www.microchipdirect.com/ProductSearch.aspx?Keywords=MCP4728

Put them on a SMD adapter for breadboarding.

Or two of these

$6 at digikey
Lot easier to interface with.

Can't do much about DIP - not much available these days outside of DUAL DACs.

dhenry:
You can use a mcu digital-out pin to power a divider. And pick your divider to get the desired output levels.

If you need precision output levels, use the digital-out pins to power a voltage reference and then use the reference to power a divider. This approach requires multiple voltage references.

An alternative is to use 1 voltage reference and then pull a divider to ground with the digital-out pin.

Each approach can be made to work.

These approaches require a lot of resistors on my pcb.

Grumpy_Mike:
All you need is the same time constant so R=1 kOhm, C=47 uF is the same as R=10 kOhm, C=4.7 uF or R=100 kOhm, C=0.47 uF.
What will change is the output impedance of the filter, but if you are using a buffer then that will be no problem.

However the TLV5620 have a voltage output so it is a simple matter to arrange some sort of buffer if the output impedance is too high for your application.

A small R requires a big C. A big R requires a impedance buffer.

The buffer requires additional components (R or Opamp) and in my hunt for less components I decided to look for a RC filter without buffer (impedance of the RC filter has to be << 15 kOhm). C can be smaller with high PWM frequency.

KeithRB:
Also, it will take some time for the PWM to charge the filter and get the value to settle. How quickly do you need it to respond? Any time you talk about Kohm resistors and uF, you can expect to wait a while!

A RC filter with R=1kOhm and C=47uF has a risetime of 0.1 s and a ripple of 0.05 V. This fits within my specifications (I can wait for 1 s for stable voltage level)

CrossRoads:
Or two of these
AD5308 Datasheet and Product Info | Analog Devices
$6 at digikey
Lot easier to interface with.

Can't do much about DIP - not much available these days outside of DUAL DACs.

The MCP4728 is a good solution: low cost, straight forward
The AD5308 is too expensive

The only think that annoys me is that these DAC solutions can perform much better than required (8 and 12 bits instead of the required 5 bits, risetime within us -> I need 1 s ...), therefore still thinking on simpler solutions with less or low cost components.

How about the idea with fixed 32 bit buffers and shift them out at a fixed frequency?

Oldchatterman:
Is it possible to pre-program a fixed buffer of 32 bits for each voltage level (L0 to L6) and send the bits, bit by bit to a digital output, by using an interrupt routine?
Is it possible to work with 1 interrupt and write 12 digital outputs each cycle? Is it possible to reach 20 kHz? This is the only task the microcontroller has to do.

Using a 16MHz mcu and bit-bang PWM at 20kHz, you would have 400 instructions available for each interrupt cycle (16e6 / 40e3). This may be achievable (not using the Arduino core), but it doesn't leave you with much for anything else. Direct port IO is 8 bit wide and requires two instructions only. Interrupt overhead is about 50 instructions.

How about the idea with fixed 32 bit buffers and shift them out at a fixed frequency?

How does that solve your problem?

dhenry:

How about the idea with fixed 32 bit buffers and shift them out at a fixed frequency?

How does that solve your problem?

I meant shift the 32 bits stream to a RC filter. I didn't investigate it, but I think the Atmega has a hard job with 12 outputs at PWM with a reasonable frequency. Therefore I was thinking to preprogram the levels (to decrease the processing load) and decrease the counts to 32 to be able to raise the frequency on 12 outputs to > 500 Hz (preferable in the kHz range).

BenF:
Using a 16MHz mcu and bit-bang PWM at 20kHz, you would have 400 instructions available for each interrupt cycle (16e6 / 40e3). This may be achievable (not using the Arduino core), but it doesn't leave you with much for anything else. Direct port IO is 8 bit wide and requires two instructions only. Interrupt overhead is about 50 instructions.

You must help me with this calculation: 16e6/40e3 -> where is the 40e3 coming from.

The only thing the mcu has to do is supply the voltage levels and keep them stable, nothing else.

I agree with Crossroads, if the components you need to use are only available in SMD packages, then use SMD adapters so you can use them on a breadboard, stripboard or perfboard. There are lots of tutorials on SMD soldering on the web. I use a hotplate, a flux pen, solder paste and solder wick.

How about this:
SPI.transfer()s to 8 74HC595 shift registers =>64 bits, pick off ouptut in groups of 5.
Output of each shift register drives an R2R Ladder.
http://www.bourns.com/pdfs/r2r.pdf (the heart of most DACs)
and then 3 quad op-amps to make voltage output into your controller.

500 Hz is 2mS period = 32,000 clock cycles. Seems like more than enough to update 8 bytes & shift them out via PWM to create your output levels.
pseudocode:

loop(){
read current time in micros;
if (2mS elapsed){
store current time for next pass thru loop;
digitalWrite (latch_signal, LOW); // use direct port manipulation to speed it up
for (x=0 to 7){
SPI.transfer( outputArray[x]);
next x;
digitalWrite (latch_signal, HIGH);  // use direct port manipulation to speed it up
} // end transfer
} // end 2mS check
// now nearly 2 mS available to update the array before next 2mS window
} // end loop

this the essence of blink with delay. Every 2mS you do a little burst of transfers, then spend the rest of the time doing other stuff.

Trade off cost too - 8 74HC595s, 12 resistor networks, 3 quad op amps
vs 3 quad DACs
vs 2 octal DACs
and the time savings wiring it up and the real estate required.

Oldchatterman:

BenF:
Using a 16MHz mcu and bit-bang PWM at 20kHz, you would have 400 instructions available for each interrupt cycle (16e6 / 40e3). This may be achievable (not using the Arduino core), but it doesn't leave you with much for anything else. Direct port IO is 8 bit wide and requires two instructions only. Interrupt overhead is about 50 instructions.

You must help me with this calculation: 16e6/40e3 -> where is the 40e3 coming from.

For 20kHz PWM, you need a 40kHz interrupt (two half periods is one cycle). You will need to pre-compute the PWM patterns outside of the interrupt loop so all you need is a simple lookup of two bytes inside the timer interrupt handler. This will give you max 16 PWM channels (8 per byte). This is tricky code to write, but as I said it may be achievable with good coding skills.

CrossRoads:
How about this:
SPI.transfer()s to 8 74HC595 shift registers =>64 bits, pick off ouptut in groups of 5.
Output of each shift register drives an R2R Ladder.
http://www.bourns.com/pdfs/r2r.pdf (the heart of most DACs)
and then 3 quad op-amps to make voltage output into your controller.

500 Hz is 2mS period = 32,000 clock cycles. Seems like more than enough to update 8 bytes & shift them out via PWM to create your output levels.

this the essence of blink with delay. Every 2mS you do a little burst of transfers, then spend the rest of the time doing other stuff.

Trade off cost too - 8 74HC595s, 12 resistor networks, 3 quad op amps

This is definitely a no go, it can't compete with the DAC solutions. I found another alternative: DAC084s085. It has SPI and the cost are reasonable.

BenF:

Oldchatterman:

BenF:
Using a 16MHz mcu and bit-bang PWM at 20kHz, you would have 400 instructions available for each interrupt cycle (16e6 / 40e3). This may be achievable (not using the Arduino core), but it doesn't leave you with much for anything else. Direct port IO is 8 bit wide and requires two instructions only. Interrupt overhead is about 50 instructions.

You must help me with this calculation: 16e6/40e3 -> where is the 40e3 coming from.

For 20kHz PWM, you need a 40kHz interrupt (two half periods is one cycle). You will need to pre-compute the PWM patterns outside of the interrupt loop so all you need is a simple lookup of two bytes inside the timer interrupt handler. This will give you max 16 PWM channels (8 per byte). This is tricky code to write, but as I said it may be achievable with good coding skills.

Ok, thanks. I am just a beginner and this area is not my daily business, so I doubt that I have the required coding skills.

How does that solve your problem?

It can be made to work.

Let's say that each time block is 1ms. You need 32ms to send 5 bits of data.

Run your timer isr in 1ms, 2ms, 4ms, 8ms, 16ms chunks. And set pins based on the data bits for each of those data chunks - essentially PPM.

Some pseudo code:

#define ANALOG_RESOLUTION 5 //analog resoultion, in bits
#define ANALOG_CHANNEL 12 //analog channel

const unsigned short duration[ANALOG_RESOLUTION]={1000, 2000, 4000, 8000, 16000}; //duration
unsigned char analog_output[ANALOG_CHANNEL]={2, 5, 1, 19, 2, ...};

timer_isr {
  static unsigned char i=0;
  set timer to interrupt after duration[i];
  increment i;
  if i = ANALOG_RESOLUTION then i = 0;
  loop:
    if analog_output[ch] & (1<<i) then set output pin for ch;
    else clear output pin for ch;
    increment ch;
  end loop
}

If you don't like ripples, you can shrink the minimum time block from 1ms to something smaller but I would go below 30 ticks (2us for an AVR at 16Mhz).