Sunday, March 01, 2015

DACs, part 3

Here is another version of the DAC+Arduino signal generator. This version drops the idea of using interrupts, and instead sets the output values in a loop which invokes the builtin delay() and delayMicroseconds() functions. The delays are set from the period between samples of the waveform, minus a small amount to account for the time taken to execute the loop. We can make this time constant, or close to it, by precomputing all the output samples and storing them in an array.

The sine and triangle waves are quite clean, the square and sawtooth waves less so. I wonder if the MCP4921 doesn't like big voltage swings. Here are some sample outputs:


Square waves work to about 20kHz, the others to less, depending on how many samples you use.
And here is the code:

  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
// Delay-based version of the DAC driver.

#include "SPI.h"

// Waveform: define only one of the following.
#define SQUARE
//#define SAWTOOTH
//#define TRIANGLE
//#define SINE

// 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 values.
#define MIN_VALUE 0
#define MAX_VALUE 0xfff

// The desired frequency in Hz and the period in microseconds.
const unsigned long freq = 100UL;
const unsigned long period = 1000000 / freq;

// How many slices we divide the period into and the period of each.
#ifdef SQUARE
const unsigned int samples = 2;
#else
const unsigned int samples = 256;
#endif
const unsigned long sample_period = period / samples;

const double pi = 3.1415926;

// We will precompute all the sample values and store them here.
// This allows the main loop to run in a fixed time.
// Initialized in setup().
unsigned int* sample_values;

void SetValue(unsigned long value) {
  PORTB &= DAC1SSLO;
  if (value > MAX_VALUE) {
    value = MAX_VALUE;
  }
  byte data = (highByte(value) & 0x0f) | 0x30;
  SPI.transfer(data);
  data = lowByte(value);
  SPI.transfer(data);
  PORTB |= DAC1SSHI;
}

// Number of millisecond and microseconds between actions.
// Microseconds must be under 16384.
// Computed in setup.
unsigned int millis_delay;
unsigned int micros_delay;

void setup() {
  pinMode(DAC1SS, OUTPUT);
  pinMode(LDAC, OUTPUT);
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
  
  sample_values = new unsigned int[samples];
#ifdef SQUARE
  sample_values[0] = MIN_VALUE;
  sample_values[1] = MAX_VALUE;
#endif
#ifdef SAWTOOTH
  float value = (float)MIN_VALUE;
  float sample_step = ((float)MAX_VALUE - MIN_VALUE)/(samples-1);
  for (int i = 0; i < samples; ++i) {
    sample_values[i] = (unsigned int)(value + 0.5);
    value += sample_step;
  }
#endif
#ifdef TRIANGLE
  unsigned int s2 = samples/2;
  float value = (float)MIN_VALUE;
  float sample_step = ((float)MAX_VALUE - MIN_VALUE)/s2;
  for (int i = 0; i < s2; ++i) {
    sample_values[i] = (unsigned int)(value + 0.5);
    value += sample_step;
  }
  for (int i = s2; i < samples; ++i) {
    sample_values[i] = (unsigned int)(value + 0.5);
    value -= sample_step;
  }
#endif
#ifdef SINE
  float value = (float)MIN_VALUE;
  double range = ((double)MAX_VALUE - MIN_VALUE)/2;
  for (int i = 0; i < samples; ++i) {
    double s = sin((2 * pi * i) / samples);
    // We have to raise the value from [-x,x] to [0,2x]
    sample_values[i] = (unsigned int)((s + 1) * range + MIN_VALUE + 0.5);
  }
#endif

  // Set delays with a little extra for the computation time.
  unsigned long slopped_period = sample_period - 15;
  millis_delay = slopped_period / 1000;
  micros_delay = slopped_period % 1000;
}

// Current sample index
unsigned int sample_number = 0;

void loop() {
  // Toggle LDAC to set the new value.
  PORTB &= LDACLO;
  unsigned long toggle_time = micros();
  PORTB |= LDACHI;

  sample_number = ++sample_number % samples;
  SetValue(sample_values[sample_number]);

  // Delay until the next action.
  delay(millis_delay);
  delayMicroseconds(micros_delay);
}

No comments: