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

Sunday, February 22, 2015

DACs, part 1

I've been thinking of building a signal generator based on an Arduino and a DAC. As an experiment, I got hold of a MCP4921 12-bit SPI interface DAC. Connecting this up to an Arduino is really simple:

  • pins 1 and 6 to 5V.
  • pin 2 to Arduino pin 10 (or any other - this is the select line).
  • pin 3 to Arduino pin 13.
  • pin 4 to Arduino pin 11.
  • pins 5 and 7 to ground.
  • and the output is on pin 8.
My first question is how fast you can drive this. It's limited by the capabilities of the DAC and how quickly the Arduino can send data to it. The datasheet says that the MCP4921 takes about 4.5uS to settle, and to see how fast I can make the code go, I wrote a minimal sketch that toggles between a high and a low value. All values in the code are constants and there are no function calls other than setup() and loop(). So the code looks like this:

#include "SPI.h"

#define DAC1SS 10

void setup() {
  pinMode(DAC1SS, OUTPUT);
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
}

void loop() {
  digitalWrite(DAC1SS, LOW);
  SPI.transfer(0x30);
  SPI.transfer(0);
  digitalWrite(DAC1SS, HIGH);
  digitalWrite(DAC1SS, LOW);
  SPI.transfer(0x3f);
  SPI.transfer(0xff);
  digitalWrite(DAC1SS, HIGH);
}

Looking at the output on a scope, the first thing to notice is that it isn't square:


The timebase here is 5us per division, so the rise time is worse than the settling time of the DAC would suggest. It shows that the minimum time for a cycle is about 31us, so limiting the frequency to around 32kHz. For anything other that square waves, you would need multiple steps, further reducing the effective frequency. The frequency seems consistent with this project, which is working with 22kHz samples. You might be able to squeeze a little but more by running the SPI interface faster, using SPI.setClockDivider(SPI_CLOCK_DIV2) in setup(). This does make a difference, but a tiny one.

Tuesday, February 17, 2015

Very, very small

Many of the interesting ICs you can get only come in surface mount packages like SOIC or TSSOP. For example, I was looking at this interesting device only to find there is no through hole version. These packages are physically much smaller than through hole ones, and as it's already hard to see what I'm soldering with my aged eyes, I had more or less given up on them. But all is not lost. I bought a simple headset magnifier from Digikey for $10, and it is good enough for me to work with these packages and even with the teeny tiny capacitors and resistors that go with them. For learning surface mount soldering, Jameco have a kit and a video (by someone who is really irritating, but useful all the same). Here's a small 555 circuit from the kit:

Not pretty, but it works. Here what the headset magnifier looks like:
This is not to the same scale as the picture above, just in case you thought my head was 4cm in diameter.

Tuesday, February 10, 2015

Beyond the noodlescope

A few months back, when I was starting to get interested in building electronic circuits again, I thought it would be useful to have an oscilloscope. I came across several web pages describing how to use the headphone/microphone socket on an Android phone with some open source software to make a really cheap scope that would work at audio bandwidths. Based on one of the pages (which I can't find now), I knocked up a 10:1 probe consisting of a FET, a capacitors for uncoupling the DC and some resistors. I tried this with a couple of programs: OsciPrime and XYZ-Apps Oscilloscope, running on a very old Nexus 1.

I connected it up to a small, fixed frequency oscillator with sine, square and triangle wave output, this Velleman kit. Here's how the sine wave looks on OsciPrime:

If you can tell from the out of focus photograph, it looks OK.

Here's the triangle wave:
As you can see, it's different from the sine wave, but far from triangular.

Here is the alleged square wave:
This is surely the most horrible square wave in the world. And it's strange as well. If it was just losing the higher harmonics, you would expect each pulse to be symmetrical in time.

Many months later, a kind friend made me guardian of his old Tektronics 465 so now I can see what the Velleman signal generator is really producing. Here's the square and triangle waves:
You can see the square wave is pretty clean (as you'd expect - the circuit is a 555 and a filter network to remove the harmonics). The triangle wave is a bit soft edges, but is also noticeably more symmetric.

So what was going on with the square wave and to a lesser degree with the triangle wave? Based on some simulation, I think it that as well as losing the higher harmonics, there is a phase shift on the remaining ones, and the shift is different at each frequency. 45 degrees on each of the odd harmonics results in something close to the observed square wave. I assume the Androids audio circuitry or the software behind it does this. I wonder why.