[Solved] Using a DAC (MCP4911) instead of a low-pass filter

I thought I'd make a new topic, as my last one got messy.

I have decided to use a true D/A converter to convert my Arduino's PWM output to analog, rather than the low-pass filter I already have set up. But I am not sure how to go about using it, as the one I have ordered uses SPI... and my Ethernet Shield uses SPI as well.

The single PWM output, after being smoothed, will go into a scaling circuit (which scales 0-5V to +/-10V), and then into a servo-amplifier, the output being a control signal. I do not think my control loop will be running at anything faster than 100 Hz, and I am hoping to write to a microSD card at the same rate.

My question is - if the Ethernet Shield is using SPI, and the DAC should be using SPI, would it be better to connect the DAC to the SPI pins (with a different SS, of course!), or go with a software approach, using maybe the ShiftOut() function, or a library like the one I attached in a previous post. I've spent the whole night reading old topics about different approaches to SPI, and the datasheet for the DAC I ordered, but I am no closer to a decision than I was several hours ago! I would really appreciate some advice :slight_smile:

Thanks,
+-

There's no reason the two shouldn't both work on the hardware SPI as long as you deal with the SS issue. OTOH your code may be a bit simpler if you use shiftOut() because your won't have to deal with SS.

As long as you don't need speed I see no real advantage either way.

At what rate do you need to write to the DAC?


Rob

SPI, fast & simple

digitalWrite ( DAC_SS, LOW ); // connects to device's LDAC pin
SPI.transfer ( command_data );
SPI.transfer ( low_data );
digitalWrite ( DAC_SS, LOW );

where the upper 4 bits of command_data are 0111 00xx typical per datasheet page 24, with xx being the upper 2 bits of data
and low_data is the remaining 8 bits of data.

If your analog data is stored as an int, then the data to be transferred out could be set up as follows:
your_data = your_data | B0111 0000 0000 0000; // sets the needed command bits high, leaves your 10 bit data alone
your_data = your_data & B0111 0011 1111 1111; // clears the needed command bits low, leaves your 10 bit data alone
(spaces shown for clarity only)

digitalWrite ( DAC_SS, LOW ); // connects to device's LDAC pin
SPI.transfer ( highByte( your_data ) );
SPI.transfer ( lowByte( your_data ) );
digitalWrite ( DAC_SS, LOW );

Both of the following pages referenced on the original thread mention a problem
with the Ethernet shield not relinquishing control of the SPI buss properly. Can
someone address this matter. Is it fixed, etc ????

Graynomad:
There's no reason the two shouldn't both work on the hardware SPI as long as you deal with the SS issue. OTOH your code may be a bit simpler if you use shiftOut() because your won't have to deal with SS.

As long as you don't need speed I see no real advantage either way.

At what rate do you need to write to the DAC?


Rob

Well I want to put my controller in a loop that lasts 10 ms (at least, I haven't determined my sampling interval yet), so at most 100 times a second. Does the DAC hold the last value received until another one is received? I think I read that it does, somewhere...

I'm a bit worried about the Ethernet Shield having one of its SPI pins low for long durations of time, I think it's the SS pin, which would mean that I can't use the DAC while the E. Shield is doing that.

CrossRoads:
SPI, fast & simple
http://ww1.microchip.com/downloads/en/DeviceDoc/22248a.pdf

digitalWrite ( DAC_SS, LOW ); // connects to device's LDAC pin
SPI.transfer ( command_data );
SPI.transfer ( low_data );
digitalWrite ( DAC_SS, LOW );

where the upper 4 bits of command_data are 0111 00xx typical per datasheet page 24, with xx being the upper 2 bits of data
and low_data is the remaining 8 bits of data.

Sorry, why is the LDAC pin being used as the SS, rather than the CS pin? I don't follow :blush:

CrossRoads:
If your analog data is stored as an int, then the data to be transferred out could be set up as follows:
your_data = your_data | B0111 0000 0000 0000; // sets the needed command bits high, leaves your 10 bit data alone
your_data = your_data & B0111 0011 1111 1111; // clears the needed command bits low, leaves your 10 bit data alone
(spaces shown for clarity only)

digitalWrite ( DAC_SS, LOW ); // connects to device's LDAC pin
SPI.transfer ( highByte( your_data ) );
SPI.transfer ( lowByte( your_data ) );
digitalWrite ( DAC_SS, HIGH );

I was using 10-bit PWM, so my analogWrite() values would be an int from 0 to 1023. Do the 2 your_data lines convert the integer to binary?

I have a feeling this is something I will need to test with a breadboard when my chips get here...

Thanks for the help so far! :slight_smile:

You are right CS as CS, vs LDAC.

Yes, these 2 lines use the SPI hardware to shift out two bytes:

SPI.transfer ( highByte( your_data ) );
SPI.transfer ( lowByte( your_data ) );

CrossRoads:
You are right CS as CS, vs LDAC.

Yes, these 2 lines use the SPI hardware to shift out two bytes:

SPI.transfer ( highByte( your_data ) );
SPI.transfer ( lowByte( your_data ) );

So in total I have 4 pins - CS, LDAC, SDI (MOSI) and SCK. I found and edited a library for a similar DAC to mine a couple of days ago, which uses SPI.transfer, so I think it could work. Originally you only had to specify the CS and LDAC pins, but because I was unsure about the remaining pins, I edited it so I could choose the MOSI and SCK pins.

I will be using a Mega with my Ethernet shield, so there will be free connections for hardware SPI. Given the relatively low rate that I will be sending data to the DAC at, would it be okay to just use the same MOSI/SCK pins?

Thanks :smiley:

.cpp:

// ----------------------------------------------------------------------------
// MCP4911.cpp
//
// Provides an SPI based interface to the MCP4911 single voltage output digital 
// to analog converter.
//
// Author: Will Dickson, IO Rodeo Inc., edited by plusminus_
// ----------------------------------------------------------------------------

#include "Arduino.h"
#include "SPI.h"
#include "MCP4911.h"

#define ACTIVE      0b0001000000000000
#define SHUTDOWN    0b0000000000000000
#define GAIN_1X     0b0010000000000000
#define DAC_A_WRITE 0b0000000000000000

MCP4911::MCP4911() {
}

// ----------------------------------------------------------------------------
// MCP4911::MCP4911
//
// Constructor
// ----------------------------------------------------------------------------
MCP4911::MCP4911(int csPin, int ldacPin, int clkPin, int sdiPin) {
    // Configure chip select and latch pins
	// I think MISO can be ignored
    cs = csPin;
    ldac = ldacPin;
	clk = clkPin;
	sdi = sdiPin;
}

// ----------------------------------------------------------------------------
// MCP4911::MCP4911
//
// initialization function
// ----------------------------------------------------------------------------
void MCP4911::begin() {
	pinMode(cs,OUTPUT);
    pinMode(ldac,OUTPUT);
	pinMode(clk,OUTPUT);
	pinMode(sdi,OUTPUT);
	
    digitalWrite(cs,HIGH);
    digitalWrite(ldac,HIGH);
	digitalWrite(clk,LOW);
	digitalWrite(sdi,LOW);
	
    // Set to default configuration
    setGain1X_A();
}

// ---------------------------------------------------------------------------
// MCP4911::getCmdWrd
//
// Gets command word for writing value to given channel
// ---------------------------------------------------------------------------
int MCP4911::getCmdWrd(int dac, int value) {
    int cmdWrd = 0;
    switch (dac) {
        case MCP4911_DAC_A:
            cmdWrd = DAC_A_WRITE;
            break;
        default:
            return 0;
    } 
    cmdWrd |= gain[dac];
    cmdWrd |= ACTIVE;
    cmdWrd |= (0b0000111111111111 & value);
    return cmdWrd;
}

// ----------------------------------------------------------------------------
// MCP4911::setValue
//
// Set the output value of the given dac channel
// ----------------------------------------------------------------------------
void MCP4911::setValue(int dac, int value) {
    int cmdWrd;
    uint8_t byte0;
    uint8_t byte1;
    
    // Enable SPI communications 
    digitalWrite(cs,LOW);
    // Send command word
    cmdWrd = getCmdWrd(dac,value);
    byte0 = cmdWrd >> 8;
    byte1 = cmdWrd & 0b0000000011111111;
    SPI.transfer(byte0);
    SPI.transfer(byte1);
    // Disable SPI communications
    digitalWrite(cs,HIGH);
    // Latch value
    digitalWrite(ldac,LOW);
    digitalWrite(ldac,HIGH);
}

// ---------------------------------------------------------------------------
// MCP4911::setValue_A
//
// Set the output value of dac A to the given value
// ---------------------------------------------------------------------------
void MCP4911::setValue_A(int value) {
    setValue(MCP4911_DAC_A, value);
}

// ---------------------------------------------------------------------------
// MCP4911::setGain1X
//
// Set the gain of the given channel to 1X
// ----------------------------------------------------------------------------
void MCP4911::setGain1X(int dac) {
    if (dac == MCP4911_DAC_A) {
        gain[dac] = GAIN_1X;
    }
}

// ----------------------------------------------------------------------------
// MCP4911::setGain1X_A
//
// Set the gain of dac A to 1X
// ----------------------------------------------------------------------------
void MCP4911::setGain1X_A() {
    setGain1X(MCP4911_DAC_A);
}

// ----------------------------------------------------------------------------
// MCP4911::off
//
// Turn off the given dac
// ----------------------------------------------------------------------------
void MCP4911::off(int dac) {
    int cmdWrd = 0;
    uint8_t byte0;
    uint8_t byte1;
    
    // Create shutdown cmd word. 
    switch (dac) {
        case MCP4911_DAC_A:
            cmdWrd = DAC_A_WRITE;
            break;
        default:
            return;
    } 
    cmdWrd |= SHUTDOWN;
    // Enable SPI communications 
    digitalWrite(cs,LOW);
    // Send command word
    byte0 = cmdWrd >> 8;
    byte1 = cmdWrd & 0b0000000011111111;
    SPI.transfer(byte0);
    SPI.transfer(byte1);
    // Disable SPI communications
    digitalWrite(cs,HIGH);
}

// ----------------------------------------------------------------------------
// MCP4911::off_A
//
// Turn off dac A
// ----------------------------------------------------------------------------
void MCP4911::off_A() {
    off(MCP4911_DAC_A);
}

.h:

// ----------------------------------------------------------------------------
// mcp4911.h
//
// Provides an SPI based interface to the MCP4911 single voltage output digital 
// to analog to converter.
//
// Author: Will Dickson, IO Rodeo Inc., edited by plusminus_
// ----------------------------------------------------------------------------
#ifndef _MCP4911_H_
#define _MCP4911_H_

#define MCP4911_NUMCHAN 1

enum MCP4911_DAC_CHAN {MCP4911_DAC_A};

class MCP4911 {
private:
    int cs;
    int ldac;
	int clkPin;
	int sdiPin;
    int gain[MCP4911_NUMCHAN];
    int getCmdWrd(int dac, int value);
public:
    MCP4911();
    MCP4911(int csPin, int ldacPin, int clkPin, int sdiPin);
    void begin();
    void setValue(int dac, int value);
    void setValue_A(int value);
    void setGain1X(int dac);
    void setGain1X_A();
    void off_A();
    void off(int dac);
};
#endif

example sketch:

#include <SPI.h>
#include "MCP4911.h"

#define AOUT_CS 5
#define AOUT_LDAC 6
#define AOUT_CLK 7
#define AOUT_SDI 8
// For example, these will change

MCP4911 analogOut = MCP4911(AOUT_CS, AOUT_LDAC, AOUT_CLK, AOUT_SDI);

void setup() {
    // Setup serial and SPI communications
    Serial.begin(115200);
    analogOut.begin();
    // SPI.setDataMode(SPI_MODE0); - default
    // SPI.setBitOrder(MSBFIRST); - default
    // SPI.setClockDivider(SPI_CLOCK_DIV8); - half default speed
    SPI.begin();

    // Configure analog outputs
    analogOut.setGain1X_A();
}


void loop() {
    static int cnt=0;
    analogOut.setValue_A(cnt);
    cnt += 20;
    if (cnt > 1023) {
        cnt = 0;
    }
}

Wow, that is a lot of code to send two bytes to a DAC.

This part

#include <SPI.h>
#include "MCP4911.h"

#define AOUT_CS 5
#define AOUT_LDAC 6
#define AOUT_CLK 7
#define AOUT_SDI 8

The SPI pins are specific hardware pins. On a '328, D13-12-11 and typically 10 for CS.
On a '2560, something like D53-52-51 and 50 somewhere in that range).
On a 1284, D13-12-11-10 for Bobuino, and D7-6-5-4 for other versions.
Not sure where on a 32U4 (Leonardo).

How will those "# define"s work when SPI.transfers are actually being used, as in below?

    // Enable SPI communications 
    digitalWrite(cs,LOW);
    // Send command word
    byte0 = cmdWrd >> 8;
    byte1 = cmdWrd & 0b0000000011111111;
    SPI.transfer(byte0);
    SPI.transfer(byte1);
    // Disable SPI communications
    digitalWrite(cs,HIGH);

Could be I am totally misunderstanding this as well, really the first time I looked at what goes into a library/class whatever it is called that you crafted.

CrossRoads:
Wow, that is a lot of code to send two bytes to a DAC.

This part

#include <SPI.h>

#include "MCP4911.h"

#define AOUT_CS 5
#define AOUT_LDAC 6
#define AOUT_CLK 7
#define AOUT_SDI 8



The SPI pins are specific hardware pins. On a '328, D13-12-11 and typically 10 for CS.
On a '2560, something like D53-52-51 and 50 somewhere in that range).
On a 1284, D13-12-11-10 for Bobuino, and D7-6-5-4 for other versions.
Not sure where on a 32U4 (Leonardo).

How will those "# define"s work when SPI.transfers are actually being used, as in below?


// Enable SPI communications
    digitalWrite(cs,LOW);
    // Send command word
    byte0 = cmdWrd >> 8;
    byte1 = cmdWrd & 0b0000000011111111;
    SPI.transfer(byte0);
    SPI.transfer(byte1);
    // Disable SPI communications
    digitalWrite(cs,HIGH);




Could be I am totally misunderstanding this as well, really the first time I looked at what goes into a library/class whatever it is called that you crafted.

The CLK and SDI defines were ones I added. But since SPI.transfer uses the hardware pins (something I completely overlooked!), I will remove all references to the pins I tried to define! I hope that when my chip gets here it will work well with the library, if not... well I'll have to take another look at software SPI, I think.

So I connect:
SDI (MOSI) - 51
SCK - 52
And I get to choose CS (SS), and LDAC. I think I understand.

I have never delved into libraries either, everything is so new and complicated to me! Thank you for your help :slight_smile:

Yes, SCK/MISO/MOSI determined by the hardware, CS/LDAC can be anywhere.

Hi, my chips arrived today!

I tested it on its own, and it works fine (it seems to be using 12-bit values and it doesn't quite go up to 5V, but I think that's because I'm running of USB right now).

I tried to fuse the example with my webserver code, and it sort of works... I put this in loop():

  unsigned long currentMillis = millis();
  
  if(currentMillis - previousMillis == 500) {
    previousMillis = currentMillis;
    analogOut.setValue_A(cnt);
    cnt += 20;
    if (cnt > 4095) {
      cnt = 0;
    }
  }

The voltage increases as expected... until I try to load my page! The voltage stays constant after that.

I'm really hoping that my final implementation works properly. The voltage will not be increasing at a constant rate, but will be decided by a controller several times a second. Hopefully loading a page will not affect this!

But if it does... is there a way to specify a different MOSI pin for my DAC? I'm guessing that's where the conflict is coming from...

Edit: I added a println() to see what was going on. When I load a page it stops changing cnt. I won't be using cnt in my final sketch so I don't think it will be a problem! :disappointed_relieved:

Edit2: It seems the webserver stops whatever I have in the if statement, I'll have to fix this...

Edit3: Everything works fine if I use >= instead of ==, which I suppose is okay, the page doesn't refresh anyway. Crisis averted! But my controllers do use a constant sampling interval...

Epilogue: On Friday I ran my controllers properly, the DAC worked absolutely beautifully. I am still serving a page stored on the microSD card, but thanks to JavaScript I don't need to refresh, so there does not appear to be any conflict with the DAC.

Thanks! :smiley:

Yes, that >= would be key. Otherwise you'd have to hit the comparison dead-on every time, and once you miss the first one there is no way to recover, as the difference would just keep increasing.

"a different MOSI pin for my DAC", nope, SCK/MISO/MOSI come from specific hardware pins. Only SS can be elsewhere.

Glad its working for you. I missed the post on the 10th, would have answered.