Saturday, February 28, 2015

DACs, part 2

Since the previous post, I've made a slight change to the circuit I use, and written code for getting the DAC to generate various waveforms. The circuit change is a simple one: connecting the -LDAC pin of the DAC to output 8 of the Arduino. The function of this pin is that it causes changes in the input to only be reflected in the output when it is pulled low.

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;
    }
  }
}

No comments: