User Tools

Site Tools


diy_spi_dac

DIY Arduino DAC – 8 Bits for 0,85€

Digital to analog conversion with Arduino can be tricky. PWM has its limitations when used for applications like audio, and R2R ladders are interesting but have a too large pin cost : 8 pins gathers 60% of the Arduino Uno disgital IO budget. The cleanest solution relies on usage of dedicated chips like the MCP4921, which allows through SPI to handle a 12bits DAC for roughly 4€. High speed SPI output capability with a pin count limited to the 3 required SPI interface.

This taken into account, it is easy to build an 8 bit SPI DAC using a R2R network connected to a deserialiser like the 74HC595 chip. This 0,85€ DAC schematic is explained herebelow.

Schematic

Arduino SPI hardware interface is connected to the deserializer (CLK/MOSI), including the CS gate connected to Arduino pin 10. This DAC has a 0/2,5V span, which allows the output amplifier to be powered with Arduino 5V Vcc without hitting the high output rail of the amp. Should the user want to use the full 0/5V range, the amplifier power supply should be raised above 8 volts to avoid hard clipping of the output. Usage of high precision resistors is mandatory to have a good linearity of the DAC, but this shall work properly with classical resistors, making this a cheap and easy way to have an 8 bits DAC.

Talking about lineraity, it’s not as good as a solid state converter, but the speed of this DAC thanks to the 74HC595 has been raised up to 332kHz.

By vertically connecting the resistors , one can get a reasonably small size on PCB, allowing design of simple shields like the one showed below: mixing a DAC, the output amplifier and a Midi receiver section with Optocoupler connected to Arduino serial Rx.

(Note : The schematic also shows a optocoupler used for Midi message reception, this has no direct link to the DAC purpose of this article)

Here is a picture of the prototype realised on veroboard. Concentrating the resistors on a veroboard allowed to leave more space on the breadboard for prototyping.

fullmaj.rd-h.fr_wordpress_wp-content_uploads_2011_07_dac_workbench.jpg

Upgrading the DIY R2R DAC

A word on the extension capability : by chaining 74HC595, it is very easy to get higher resolution DACs in theory. But in practice, taking into account that the accuracy of the resistors has to be at least equal to the DAC quantum (unity bit), a 16 bit DAC shall not provide the expected results, this is shown in the table below:

Bits 2 4 6 8 10 12 16
Accuracy 25% 6,25%1,56%0,39% 0,1% 0,02% 0,002%

6 bit DAC can be achieved accurately with 1% resistors. Above, you need 0,5% resistors for 8 bits DAC, and so on. Using 1% resistors for 8 bits is Ok anyhow, but increasing the number of bits leads to erratic behaviour.

So how does it work? Well, it’s doing well as you can see on the right. There’s still some issues linked to the 011111111 to 10000000 transition because of resistor inaccuracies, but this shall provide help to anyone of us Arduino users in the quest of getting a cheap but still efficient DAC!

Example : How to use the DAC as a DDS

To operate the DAC as a DDS, you just have to configure the SPI to communicate with the 74HC595 as follow.

insert this code in the setup() section of your Arduino sketch.

  // SPI Link Setup : Highest Speed Possible indeed !
  // Go Go Go !!
  byte clr;
  SPCR |= ( (1<<SPE) | (1<<MSTR) ); // enable SPI as master
  SPCR &= ~( (1<<SPR1) | (1<<SPR0) ); // clear prescaler bits
  clr=SPSR; // clear SPI status reg
  clr=SPDR; // clear SPI data reg
  SPSR |= (1<<SPI2X); // set prescaler bits
  delay(100);

f you afterwards want to use it as a DDS to generate sine Waves, you need 3 things:

  • A 8 bit sine Look Up Table.
  • A counter to setup an interrupt
  • An interrupt handler to update the output sample.

Here we go, I used Timer2 configured as follow:

  // Configuration du Timer 2 pour la fréquence d'échantillonnage
  // No output (COM2A0 cleared, set = Toggle on Match)
  // Compteur en FAST PWM, Clear on Match OCR2A, Update at BOTTOM
  // Clock /8 (CS21 set, si set CS20 en plus /32)
  TCCR2A = _BV(COM2A0) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22)  | _BV(CS21);
  TIMSK2 = _BV(TOIE2); // Enable timer overflow interrupt, once every 1/22039Hz = 45.375us.
  OCR2A = SMP_REG; // Use the right value here to define the sampling frequency

Then you create your Look Up Tables for the output, for example:

const unsigned char LUT_sin[]={
  128,131,134,137,140,143,146,149,152,155,158,162,165,167,170,173,176,179,182,185,188,190,193,196,198,201,203,206,208,211,213,215,218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,244,245,246,248,249,250,250,251,252,253,253,254,254,254,255,255,255,255,255,255,255,254,254,254,253,253,252,251,250,250,249,248,246,245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,220,218,215,213,211,208,206,203,201,198,196,193,190,188,185,182,179,176,173,170,167,165,162,158,155,152,149,146,143,140,137,134,131,128,124,121,118,115,112,109,106,103,100,97,93,90,88,85,82,79,76,73,70,67,65,62,59,57,54,52,49,47,44,42,40,37,35,33,31,29,27,25,23,21,20,18,17,15,14,12,11,10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,17,18,20,21,23,25,27,29,31,33,35,37,40,42,44,47,49,52,54,57,59,62,65,67,70,73,76,79,82,85,88,90,93,97,100,103,106,109,112,115,118,121,124};
const unsigned char LUT_log[]={
  0,32,50,64,74,82,89,95,101,106,110,114,118,121,124,127,130,133,135,138,140,142,144,146,148,150,151,153,155,156,158,159,161,162,163,165,166,167,168,169,170,172,173,174,175,176,177,178,179,180,180,181,182,183,184,185,186,186,187,188,189,189,190,191,192,192,193,194,194,195,196,196,197,198,198,199,199,200,201,201,202,202,203,203,204,204,205,206,206,207,207,208,208,209,209,210,210,210,211,211,212,212,213,213,214,214,215,215,215,216,216,217,217,217,218,218,219,219,219,220,220,221,221,221,222,222,222,223,223,223,224,224,224,225,225,226,226,226,227,227,227,228,228,228,228,229,229,229,230,230,230,231,231,231,232,232,232,232,233,233,233,234,234,234,234,235,235,235,235,236,236,236,237,237,237,237,238,238,238,238,239,239,239,239,240,240,240,240,241,241,241,241,242,242,242,242,243,243,243,243,243,244,244,244,244,245,245,245,245,245,246,246,246,246,247,247,247,247,247,248,248,248,248,248,249,249,249,249,249,250,250,250,250,250,251,251,251,251,251,252,252,252,252,252,253,253,253,253,253,253,254,254,254,254,254,255};
const unsigned int LUT_freq[]={
  98,103,110,116,123,130,138,146,155,164,174,184,195,207,219,232,246,261,276,293,310,328,348,369,391,414,438,465,492,521,552,585,620,657,696,737,781,828,877,929,984,1043,1105,1171,1240,1314,1392,1475,1563,1655,1754,1858,1969,2086,2210,2341,2480,2628,2784,2950,3125,3311,3508,3716,3937,4172,4420,4682,4961,5256,5568,5899,6250,6622,7016,7433,7875,8343,8839,9365,9922,10512,11137}; 

We're almost there, since the Timer2 above has been configured to trigger an interrupt, here is the interrupt vector code:

// It
ISR(TIMER2_OVF_vect)
{
  acc += delta_acc; // update of phase accumulator
  sample = LUT_sin[acc>>8]; // >>8 bcz 'acc' is an int
  spi_transfer(sample);
  PORTB &= ~(4); // Fast RCK Toggling using direct port toggling
  PORTB |= 4;
}

There you are, you have a custom DDC with a DIY DAC for less than a Euro.

Congratulations.

diy_spi_dac.txt · Last modified: 2018/08/26 11:36 (external edit)