1

I'm using a 2-channel, 12-bit DAC (MCP4922, datasheet) with an Arduino Uno, using the library written for it here. With the clock divider set to 2, I should have the SPI clock frequency at 8MHz. If each value sent to the DAC takes about 12 of these clock cycles, that leaves me with about 650kHz as my output frequency (when I'm sending a simple on-off signal like below), but the fastest I see is about 25kHz.

void loop() { dac.output2(4095, 0); dac.output2(0, 4095); }

Am I misunderstanding how the transfer protocol works? How can I make this faster? I'd like to both understand why this is so slow, and also to get an output frequency of at least 50kHz.

  • 1) Did you set the clock divider to 2, or to SPI_CLOCK_DIV2? 2) Did you setPortWrite(true)? – Edgar Bonet Jun 14 '17 at 10:11
  • @EdgarBonet Yes to both, unfortunately. – janizer Jun 14 '17 at 14:20
1

Let's do some maths...

  • 16MHz master clock / 2 = 8MHz SPI clock
  • 2 transactions = 16 bits (not 12!)
  • 8,000,000 / 16 = 500,000
  • 2 transitions per cycle
  • 2 channels
  • 2 + 2 = 4
  • 500,000 / 4 = 125,000Hz

So in a perfect world you could expect 125kHz from the DAC. However the world is far from perfect.

Let's look at what it's actually doing when you use output2():

void DAC_MCP49xx::output2(unsigned short data_A, unsigned short data_B) {
  this->_output(data_A, CHANNEL_A);
  this->_output(data_B, CHANNEL_B);

  // Update the output, if desired.
  // The reason this is only in the dual-output version is simple: it's mostly useless
  // for the single-output version, as it would make more sense to tie the \LDAC pin
  // to ground, or do it manually. However, there should be a single-call method
  // to update *both* channels in sync, which wouldn't be possible with multiple
  // separate DACs (for which there is latch()).
  if (automaticallyLatchDual) {
    this->latch();
  }
}

Assuming automaticallyLatchDual is false we can pretty much ignore that section. So it's calling _output() twice - once for each channel. And what does that do?

void DAC_MCP49xx::_output(unsigned short data, Channel chan) {
  // Truncate the unused bits to fit the 8/10/12 bits the DAC accepts
  if (this->bitwidth == 12)
    data &= 0xfff;
  else if (this->bitwidth == 10)
    data &= 0x3ff;
  else if (this->bitwidth == 8)
    data &= 0xff;

  // Drive chip select low
  if (this->port_write)
    PORTB &= 0xfb; // Clear PORTB pin 2 = arduino pin 10
  else
    digitalWrite(ss_pin, LOW); 

  // bit 15: 0 for DAC A, 1 for DAC B. (Always 0 for MCP49x1.)
  // bit 14: buffer VREF?
  // bit 13: gain bit; 0 for 1x gain, 1 for 2x (thus we NOT the variable)
  // bit 12: shutdown bit. 1 for active operation
  // bits 11 through 0: data 
  uint16_t out = (chan << 15) | (this->bufferVref << 14) | ((!this->gain2x) << 13) | (1 << 12) | (data << (12 - this->bitwidth));

  // Send the command and data bits
  SPI.transfer((out & 0xff00) >> 8);
  SPI.transfer(out & 0xff);

  // Return chip select to high
  if (this->port_write)
    PORTB |= (1 << 2); // set PORTB pin 2 = arduino pin 10
  else
    digitalWrite(ss_pin, HIGH);
}

That's quite a lot. All those boolean mathematics operations and decision making slows things down a lot. So does hopping from function to function (yes, the compiler will possibly try and inline some things).

Even the SPI.transfer() isn't that efficient with checks and while loops, etc.

To get anywhere near the kind of speed you are looking for you would need to use non-blocking SPI code so you can be transferring a byte out of the SPI port at the same time as getting the next byte ready to send. You don't need to wait for the current SPI transfer to finish (as the SPI library does), you just need to be sure that the previous one has finished before you start the next one.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.