After experimenting with various options, I decided to write the code in an interrupt-based version. It uses an internal timer of the Arduino to call an interrupt routine every N microseconds. The interrupt routine just increments a variable for how many time it has been called, and in addition if a new value is ready for output, it pulses -LDAC to make it take effect. This means you can do the calculations for the next value to output without needing to worry about timing, then send the new value to the DAC ready for the next timing interrupt.

Lots of code follows. It will generate square, sawtooth, triangle and sine wave output. It's pretty limited: square waves only go to 10kHz reliably, and the other waveforms to much lower frequencies, the exact value depending on the number of samples you ask for. Too many samples and some will get skipped, or you get timing jitter. The sine wave is the worst, though it could be considerably improved by getting the values from a lookup table rather than the sin() function. If you want to see how to do this much better than I did, take a look at http://www.instructables.com/id/Arduino-Waveform-Generator/?ALLSTEPS, a really nice piece of work. I also used this code as an excuse for coding closer to the hardware, for example setting output pins by writing to the ports rather than using the digitalWrite() function. I also made some use of types such as unsigned long to get better precision in the calculations without resorting to floating point calculation, and unsigned int for many of the other variables. You have to be quite careful when coding this way, and use casts to avoid truncating values early.

(Code formatted using http://hilite.me/. It does a nice job. I'm not sure I like including code as opposed to giving a download link - opinions welcome, should anyone actually be reading this :-))

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 // Interrupt-based version of the DAC driver. #include "SPI.h" // Waveform: define only one of the following. #undef SQUARE #undef SAWTOOTH #undef TRIANGLE #define SINE // The MCP4921 has a 12 bit range. We do all calculations of the output value // using values which are 65536 times the value we want. This gets us more precision // without needing to use floating point arithmetic, provided we calculate with longs. #define VALUESHIFT 16 // SS pin for the DAC. #define DAC1SS 10 // Mask to select SS PIN on port b. #define DAC1SSHI B100 #define DAC1SSLO B11111011 // Pin and mask for LDAC pin. #define LDAC 9 #define LDACHI B10 #define LDACLO B11111101 // Range of the DAC. #define MIN_DAC 0 #define MAX_DAC 0xfff #define MIN_VALUE ((unsigned long)MIN_DAC << VALUESHIFT) #define MAX_VALUE ((unsigned long)MAX_DAC << VALUESHIFT) // Clock frequency in Hz. const unsigned long clock_freq = 16000000UL; // Set this to the desired frequency in Hz. const unsigned long freq = 30UL; // Set this to the number of samples per cycle (including one at the // end of a cycle). #ifdef SQUARE const unsigned int samples = 2; #else const unsigned int samples = 128; #endif const float pi = 3.1415926; // Interrupt frequency in Hz. const unsigned long interrupt_freq = freq * samples; // Using timer 1 (a 16 bit timer), the interrupt frequency is // 16000000 / (prescaler * (compare match register + 1)) // where the compare match register must be 0 to 65535, // and the prescaler can be 1, 8, 64, 256 or 1024. // Rearranging, CMR = (16000000/(prescaler * int.freq))-1 // Due to rounding, we might not get an exact value. unsigned int CMR(unsigned int prescaler, unsigned long freq) { return ((clock_freq / prescaler) / freq)-1; } unsigned long ActualFreq(unsigned int prescaler, unsigned int cmr) { return (clock_freq / prescaler) / (cmr+1); } unsigned int CMRAndError( unsigned int prescaler, unsigned long freq, unsigned long* error) { unsigned int cmr = CMR(prescaler, freq); unsigned long actual_freq = ActualFreq(prescaler, cmr); *error = (freq > actual_freq) ? freq - actual_freq : actual_freq - freq; return cmr; } // Given a prescaler value, and its cmr and error, and a new one prescaler value, // if the new one is better, update the prescaler, cmr and error. void ImproveParameters( unsigned int* prescaler, unsigned int* cmr, unsigned long* error, unsigned long freq, unsigned int new_prescaler) { if (*error == 0) { return; } unsigned long new_error; unsigned long new_cmr = CMRAndError(new_prescaler, freq, &new_error); if (new_error < *error) { *prescaler = new_prescaler; *cmr = new_cmr; *error = new_error; } } void SetValue(unsigned long value) { PORTB &= DAC1SSLO; unsigned int v = value >> VALUESHIFT; if (v > MAX_DAC) { v = MAX_DAC; } byte data = (highByte(v) & 0x0f) | 0x30; SPI.transfer(data); data = lowByte(v); SPI.transfer(data); PORTB |= DAC1SSHI; } unsigned long last_value = MIN_VALUE; unsigned long next_value = MIN_VALUE; unsigned int updates = 0; unsigned long sample_step = 0; unsigned int sample_number = 0; const unsigned int samples2 = samples / 2; float angle_step; const unsigned long range_multiplier = (MAX_VALUE - MIN_VALUE) / 2; bool have_new_value = false; void setup() { #ifdef SAWTOOTH sample_step = (((unsigned long)MAX_DAC+1 - MIN_DAC) << VALUESHIFT) / (samples-1); #endif #ifdef TRIANGLE sample_step = 2 * (((unsigned long)MAX_DAC+1 - MIN_DAC) << VALUESHIFT) / samples; #endif #ifdef SINE angle_step = 2 * pi / samples; #endif // Try each prescaler to find the best. unsigned long error; unsigned int prescaler = 1; unsigned int cmr = CMRAndError(prescaler, interrupt_freq, &error); ImproveParameters(&prescaler, &cmr, &error, interrupt_freq, 8); ImproveParameters(&prescaler, &cmr, &error, interrupt_freq, 64); ImproveParameters(&prescaler, &cmr, &error, interrupt_freq, 256); ImproveParameters(&prescaler, &cmr, &error, interrupt_freq, 1024); pinMode(DAC1SS, OUTPUT); pinMode(LDAC, OUTPUT); SPI.begin(); SPI.setBitOrder(MSBFIRST); SetValue(next_value); cli(); // Set interrupt timer. TCCR1A = 0; TCCR1B = 0; TCNT1 = 0; // Counter value OCR1A = cmr; TCCR1B |= (1 << WGM12); if (prescaler == 1) { TCCR1B |= (1 << CS10); } else if (prescaler == 8) { TCCR1B |= (1 << CS11); } else if (prescaler == 64) { TCCR1B |= (1 << CS11) | (1 << CS10); } else if (prescaler == 256) { TCCR1B |= (1 << CS12); } else { TCCR1B |= (1 << CS12) | (1 << CS10); } // Enable timer compare interrupt TIMSK1 |= (1 << OCIE1A); sei(); } ISR(TIMER1_COMPA_vect){ if (have_new_value) { PORTB &= LDACLO; have_new_value = false; PORTB |= LDACHI; } updates += 1; } void loop() { if (updates > 0) { unsigned int u = updates; updates = 0; sample_number = (sample_number + u) % samples; #ifdef SQUARE next_value = (sample_number >= samples2) ? MAX_VALUE : MIN_VALUE; #endif #ifdef SAWTOOTH next_value = MIN_VALUE + sample_number * sample_step; #endif #ifdef TRIANGLE if (sample_number < samples2) { next_value = MIN_VALUE + sample_step * sample_number; } else { next_value = MIN_VALUE + sample_step * (samples - sample_number); } #endif #ifdef SINE next_value = (unsigned long)(range_multiplier * (sin(sample_number * angle_step)+1)); #endif if (next_value != last_value) { SetValue(next_value); last_value = next_value; have_new_value = true; } } }