Using MCP4725 DAC with Arduino Uno and Rotary Encoder

Hello!
I’m new to the Arduino community and for my first project I am trying to replace a potentiometer with a quadrature rotary encoder. The encoder is being used to measure crankshaft position, and I have a sketch already that decodes the encoder into counts (0-4095). The next step for me is to send this number to the DAC which will then output a voltage (0-5 volts) which I will be able to use in my program. This is my first time using I2C and I would very much appreciate help. Please see my current sketch below.

Thanks in advance,

Gage

// Libraries
#include "Arduino.h"
#include <digitalWriteFast.h>
#include <Wire.h>
#include <Adafruit_MCP4725.h>

// Declarations
volatile int16_t EncoderTicks = 0;
volatile bool EncoderASet;
volatile bool EncoderBSet;
volatile bool EncoderAPrev;
volatile bool EncoderBPrev;

// Quadrature Encoder
#define EncoderInterruptA 0
#define EncoderInterruptB 1
#define EncoderPinA 2
#define EncoderPinB 3
#define OutputPin A5

// DAC
Adafruit_MCP4725 dac;

void setup(void)  {
  Serial.begin(9600);
  dac.begin(98); // I2C Address for this MCP4725 (0x62)

  pinMode(EncoderPinA, INPUT);      // sets pin A as input
  digitalWrite(EncoderPinA, LOW);   // turn on pullup resistors
  pinMode(EncoderPinB, INPUT);      // sets pin B as input
  digitalWrite(EncoderPinB, LOW);   // turn on pullup resistors
  attachInterrupt(EncoderInterruptA, HandleMotorInterruptA, CHANGE);
  attachInterrupt(EncoderInterruptB, HandleMotorInterruptB, CHANGE);
}

void loop() {
  Serial.print("Encoder Ticks: ");
  Serial.print(EncoderTicks);
  Serial.print("\n");
  Wire.beginTransmission(0x62);
  Wire.write(EncoderTicks);
  Wire.endTransmission();
  
}

/////////////////////////////////////////////////////////////////////////////////////

// Interrupt service routines for **clockwise rotation** when looking at encoder
void HandleMotorInterruptA()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);
  
  digitalWrite(OutputPin,EncoderTicks);
  EncoderTicks+=ParseEncoder();
  if(EncoderTicks>4095) {
    EncoderTicks=0;    // rollover ticks
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// Interrupt service routines for **counter-clockwise rotation** when looking at encoder
void HandleMotorInterruptB()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);

  digitalWrite(OutputPin,EncoderTicks);
  EncoderTicks+=ParseEncoder(); 
  if(EncoderTicks<0)  {
    EncoderTicks=+4095; // #DontBeSoNegative
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// State Table for Quadrature Encoder
int ParseEncoder()  {
  if(EncoderAPrev && EncoderBPrev)  {
    if(!EncoderASet && EncoderBSet) return 1;
    if(EncoderASet && !EncoderBSet) return -1;
  }else if(!EncoderAPrev && EncoderBPrev) {
    if(!EncoderASet && !EncoderBSet) return 1;
    if(EncoderASet && EncoderBSet) return -1;
  }else if(!EncoderAPrev && !EncoderBPrev)  {
    if(EncoderASet && !EncoderBSet) return 1;
    if(!EncoderASet && EncoderBSet) return -1;
  }else if(EncoderAPrev && !EncoderBPrev) {
    if(EncoderASet && EncoderBSet) return 1;
    if(!EncoderASet && !EncoderBSet) return -1;
  }
}

Good job on using code tags on your first posting. Welcome to the forum.

You include a library

#include <Adafruit_MCP4725.h>

you instantiate a dac object

Adafruit_MCP4725 dac;

and you bring it to life

dac.begin(98);

The only thing you do not do is use the library command to set the output of the dac.

dac.setVoltage( uint16_t output, bool writeEEPROM )

The reason to use this library command is that there is a command byte which needs to be sent to the chip as well as the value. Your simple code does not do this

Wire.beginTransmission(0x62);
  Wire.write(EncoderTicks);
  Wire.endTransmission();

The boolean writeEEPROM is to control whether or not the current voltage value is set in the mcp4725’s eeprom and will be used when the chip powers up.

You should review the library examples that come with the library. Even better, review the .cpp and .h files that make up the library. This is a very simple library, and is a good place to get an understanding of how libraries are structured and how they work.

Regarding the encoder, best practice is to disable interrupts while you read tick value to ensure you don’t grab the value while it is changing. See Nick Gammon’s excellent tutorial on interrupts
http://gammon.com.au/interrupts

Here your code modified

// Libraries
#include "Arduino.h"
#include <digitalWriteFast.h>
#include <Wire.h>
#include <Adafruit_MCP4725.h>

// Declarations
volatile int16_t EncoderTicks = 0;
int16_t readEncoderTicks = 0; //transfer variable for safe, unchanging reading
volatile bool EncoderASet;
volatile bool EncoderBSet;
volatile bool EncoderAPrev;
volatile bool EncoderBPrev;

// Quadrature Encoder
#define EncoderInterruptA 0
#define EncoderInterruptB 1
#define EncoderPinA 2
#define EncoderPinB 3
#define OutputPin A5

// DAC
Adafruit_MCP4725 dac;

void setup(void)  {
  Serial.begin(9600);
  dac.begin(98); // I2C Address for this MCP4725 (0x62)

  pinMode(EncoderPinA, INPUT);      // sets pin A as input
  digitalWrite(EncoderPinA, LOW);   // turn on pullup resistors
  pinMode(EncoderPinB, INPUT);      // sets pin B as input
  digitalWrite(EncoderPinB, LOW);   // turn on pullup resistors
  attachInterrupt(EncoderInterruptA, HandleMotorInterruptA, CHANGE);
  attachInterrupt(EncoderInterruptB, HandleMotorInterruptB, CHANGE);
}

void loop() {
  noInterrupts();
  readEncoderTicks = EncoderTicks;
  interrupts();
  Serial.print("Encoder Ticks: ");
  Serial.print(readEncoderTicks);
  Serial.print("\n");
  //Wire.beginTransmission(0x62);
  //Wire.write(EncoderTicks);
  //Wire.endTransmission();
  dac.setVoltage(readEncoderTicks, false);

}

/////////////////////////////////////////////////////////////////////////////////////

// Interrupt service routines for **clockwise rotation** when looking at encoder
void HandleMotorInterruptA()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);

  digitalWrite(OutputPin, EncoderTicks);
  EncoderTicks += ParseEncoder();
  if (EncoderTicks > 4095) {
    EncoderTicks = 0;  // rollover ticks
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// Interrupt service routines for **counter-clockwise rotation** when looking at encoder
void HandleMotorInterruptB()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);

  digitalWrite(OutputPin, EncoderTicks);
  EncoderTicks += ParseEncoder();
  if (EncoderTicks < 0)  {
    EncoderTicks = +4095; // #DontBeSoNegative
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// State Table for Quadrature Encoder
int ParseEncoder()  {
  if (EncoderAPrev && EncoderBPrev)  {
    if (!EncoderASet && EncoderBSet) return 1;
    if (EncoderASet && !EncoderBSet) return -1;
  } else if (!EncoderAPrev && EncoderBPrev) {
    if (!EncoderASet && !EncoderBSet) return 1;
    if (EncoderASet && EncoderBSet) return -1;
  } else if (!EncoderAPrev && !EncoderBPrev)  {
    if (EncoderASet && !EncoderBSet) return 1;
    if (!EncoderASet && EncoderBSet) return -1;
  } else if (EncoderAPrev && !EncoderBPrev) {
    if (EncoderASet && EncoderBSet) return 1;
    if (!EncoderASet && !EncoderBSet) return -1;
  }
}

Cattledog,

Thanks a lot for the quick response and the help. I applied your modified code and the system works flawlessly! I didn't realize there were examples in the library folder. I tried looking for examples online but didn't have much luck.

Thanks again, Gage

Hello again,

I’ve run into an issue. The system works flawlessly when rolling the encoder by hand, but when the system is in practice and the encoder is rotating quickly, the voltage reading seems to get stuck. Right at 400 RPM, the voltage reading does not change anymore.

I’m unsure what exactly is getting stuck; whether it be the MCU that can’t keep up (doubtful in my mind), the DAC that can’t convert quickly enough, or the I2C that can’t handle the RPM.

Any thoughts?

Gage

I have been experimenting with the code and a quadrature generator program which I use for testing.

At 400 rpm and 4096 counts/rev you are seeing 28700 encoder counts/second.

The encoder reading algorithm, when tested by itself, is quite capable of handling 80000 counts per second and it is not the cause of the problem. There are issues with using the serial read to determine the count value, and I'm not sure how you are determining that the voltage does not change. How are you reading the output? If you have no serial, and are reading the output of the dac with a scope it would be best.

I tested the counts observed both with and without the dac.setVoltage() command, and I believe the the use of the dac is the major problem.

At 20000cps, the counts were the same with and without the dac. At 40000 counts/per second, the values with and without the dac did not match and were consistent with the dac slowing the period of the readings down. This is consistent with your findings of 400 rpm max. At 80000 counts/per second the readings with the dac were way off and were consistent with the dac writes slowing things way down.

I don't understand the root cause of the dac.setVoltage() issue.

The i2c bus is indeed running at the 400KHz max rate. The dac write uses 3 bytes (24 bits) which gives 60 microseconds. There is handshaking in the Wire library, and other overhead so maybe its 100+ microseconds to change the voltage. At 40000 encoder counts/second you will see 4 counts in that period.

You may be able to find a 12 bit spi dac to use which would very much faster.

Perhaps there is a digital solution to your problem? Can you use the digital count output instead of volts? Can you update the output periodically, at a rate which is slower than the free running loop? How frequently do you need to know the crankshaft position? Does the rate of rotation vary within one rotation. I remember one project where someone was trying to pick up rotational speed changes at firing points within a cycle.

What precision do you need on the crankshaft position. 360 degrees/4096 is less than a 10th of a degree. How are you using the crankshaft position?

Very informative, I'm at least glad to see that you are experiencing the same limitations as me.

Perhaps there is a digital solution to your problem? Can you use the digital count output instead of volts?

The thought has crossed my mind, but it would require a bit of changes that I would like to avoid.


Can you update the output periodically, at a rate which is slower than the free running loop? How frequently do you need to know the crankshaft position? Does the rate of rotation vary within one rotation.

This has also crossed my mind. I tried to slow down the program by adding a delay(0.5) in the sketch, but it did not seem to help much. What's the easiest way I can slow down the program? 10 samples/second would be ideal, is this doable?

Regarding the use of digital output, please explain more.

The thought has crossed my mind, but it would require a bit of changes that I would like to avoid.

10 samples/second would be ideal, is this doable?

Yes, very simple with the standard non blocking millis() based timer. See http://forum.arduino.cc/index.php?topic=503368.0 and http://forum.arduino.cc/index.php?topic=223286.0

if(millis() - lastReadTime >= readInterval) //use micros() instead of millis() for more precise timing if required
{
lastReadTime += readInterval;
//take the periodic action
}

There are some other issues with your code I want to address. What Arduino are you using? On the AT328 models, A5 is an i2c pin as SDA and SCL are on the A4 and A5 pins. You call it an output but with no set pinMode, it default to an input. EncoderTicks is a number between 0 and 4096 and digitalWrite takes HIGH or LOW as a parameter so digitalWrite(OutputPin, EncoderTicks) is really turning on the internal pullup. This is probably not what you want. I’ve removed these lines in my testing.

#define OutputPin A5
digitalWrite(OutputPin, EncoderTicks);

You set the encoder output pinMode as follows

pinMode(EncoderPinA, INPUT);      // sets pin A as input
  digitalWrite(EncoderPinA, LOW);   // turn on pullup resistors

You have this backwards, and have turned the pullups off. Use this syntax

pinMode(EncoderPinA, INPUT_PULLUP);

Serial printing is slow. Use a higher baud rate. Remember to set the monitor to match. If you use a terminal program and not the serial monitor, you can probably use higher rate. Are you using Serial in the actual program?

//Serial.begin(9600)
Serial.begin(115200);

Here’s the original code modified to demonstrate the timer and my other corrections. Please post the actual code you are currently using.

// Libraries
#include "Arduino.h"
#include <digitalWriteFast.h>
#include <Wire.h>
#include <Adafruit_MCP4725.h>

// Declarations
volatile int16_t EncoderTicks = 0;
int16_t readEncoderTicks = 0; //transfer variable for safe, unchanging reading
volatile bool EncoderASet;
volatile bool EncoderBSet;
volatile bool EncoderAPrev;
volatile bool EncoderBPrev;//timer variables
unsigned long lastReadTime;
const int readInterval = 100; //100 ms for 10Hz sample rate

// Quadrature Encoder
#define EncoderInterruptA 0
#define EncoderInterruptB 1
#define EncoderPinA 2
#define EncoderPinB 3
//#define OutputPin A5

// DAC
Adafruit_MCP4725 dac;

void setup(void)  {
  //Serial.begin(9600);
  Serial.begin(115200);
  dac.begin(98); // I2C Address for this MCP4725 (0x62)

  //pinMode(EncoderPinA, INPUT);      // sets pin A as input
  //digitalWrite(EncoderPinA, LOW);   // turn on pullup resistors
  //pinMode(EncoderPinB, INPUT);      // sets pin B as input
  //digitalWrite(EncoderPinB, LOW);   // turn on pullup resistors
  pinMode(EncoderPinA, INPUT_PULLUP);
  pinMode(EncoderPinB, INPUT_PULLUP);
  attachInterrupt(EncoderInterruptA, HandleMotorInterruptA, CHANGE);
  attachInterrupt(EncoderInterruptB, HandleMotorInterruptB, CHANGE);
}

void loop()
{
  if (millis() - lastReadTime >= readInterval)
  {
    lastReadTime += readInterval;
    noInterrupts();
    readEncoderTicks = EncoderTicks;
    interrupts();
    //Serial.print("Encoder Ticks: ");//remove to minimize serial
    Serial.print(readEncoderTicks);
    Serial.print("\n");
    dac.setVoltage(readEncoderTicks, false);
  }
}

/////////////////////////////////////////////////////////////////////////////////////

// Interrupt service routines for **clockwise rotation** when looking at encoder
void HandleMotorInterruptA()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);

  //digitalWrite(OutputPin, EncoderTicks);
  EncoderTicks += ParseEncoder();
  if (EncoderTicks > 4095) {
    EncoderTicks = 0;  // rollover ticks
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// Interrupt service routines for **counter-clockwise rotation** when looking at encoder
void HandleMotorInterruptB()  {
  EncoderBSet = digitalReadFast(EncoderPinB);
  EncoderASet = digitalReadFast(EncoderPinA);

  //digitalWrite(OutputPin, EncoderTicks);
  EncoderTicks += ParseEncoder();
  if (EncoderTicks < 0)  {
    EncoderTicks = +4095; // #DontBeSoNegative
  }
  EncoderAPrev = EncoderASet;
  EncoderBPrev = EncoderBSet;
}

// State Table for Quadrature Encoder
int ParseEncoder()  {
  if (EncoderAPrev && EncoderBPrev)  {
    if (!EncoderASet && EncoderBSet) return 1;
    if (EncoderASet && !EncoderBSet) return -1;
  } else if (!EncoderAPrev && EncoderBPrev) {
    if (!EncoderASet && !EncoderBSet) return 1;
    if (EncoderASet && EncoderBSet) return -1;
  } else if (!EncoderAPrev && !EncoderBPrev)  {
    if (EncoderASet && !EncoderBSet) return 1;
    if (!EncoderASet && EncoderBSet) return -1;
  } else if (EncoderAPrev && !EncoderBPrev) {
    if (EncoderASet && EncoderBSet) return 1;
    if (!EncoderASet && !EncoderBSet) return -1;
  }
}

What Arduino are you using?

I’m using the Arduino Uno with the Atmega 328P. My current setup is attached.


Are you using Serial in the actual program?[\quote]
I am not using serial communication in real-world-application; I am only using it for debugging when plugged into my PC.


I’ve uploaded your modified code and I am starting to notice a trend… If I spin the encoder very quickly, even the serial monitor freezes up under fast RPM. I then started to notice that the TX LED on the Uno shuts off at the same time that the serial stops printing. Do you think its the AT328?

Capture.GIF

With some additional testing, I can see that there is a faster encoder reading algorithm than what you are using. It uses direct port readings, one common interrupt handler, and had very fast logic with a lookup table. In my benchmark tests of different algorithms it was the fastest.

Try this code, and if it works for you, we can go through the explanation of the algorithm.

With the dac, I can read 80K counts/second with 100 millisecond read periods and the counts with and without the dac.setVoltage() command were the same.

Unfortunately, this does not really do you any good when you are trying to determine angular position. I think, there is a fundamental problem in your situation in that you can not read the angular position every 100ms (to accomodate the dac) and get much resolution on position at the higher rotational speeds, because you overflow the 4096 counts in one interval.

I think you need to explore the digital solution (or else find an spi dac). I know that with the encoder routine suggested you can read and print a count every 100 microseconds(and probably faster) and get pretty good resolution on the angular position.

// Libraries
#include "Arduino.h"
#include <Wire.h>
#include <Adafruit_MCP4725.h>

// Declarations
const char encTable[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0}; //gives -1, 0 or 1 depending on encoder movement
volatile int16_t EncoderTicks = 0;
int16_t readEncoderTicks = 0; //transfer variable for safe, unchanging reading
volatile byte encState;
unsigned long lastReadTime;
const unsigned int readInterval = 100; //100 ms for 10Hz sample rate

// Quadrature Encoder
#define EncoderInterruptA 0
#define EncoderInterruptB 1
#define EncoderPinA 2
#define EncoderPinB 3

// DAC
Adafruit_MCP4725 dac;

void setup(void)  {
  Serial.begin(115200);
  dac.begin(98); // I2C Address for this MCP4725 (0x62)

  pinMode(EncoderPinA, INPUT_PULLUP);
  pinMode(EncoderPinB, INPUT_PULLUP);
  attachInterrupt(EncoderInterruptA, HandleMotorInterrupt, CHANGE);
  attachInterrupt(EncoderInterruptB, HandleMotorInterrupt, CHANGE);
}

void loop()
{
  if (millis() - lastReadTime >= readInterval) //change to micros?
  {
    lastReadTime += readInterval;
    noInterrupts();
    readEncoderTicks = EncoderTicks;
    //EncoderTicks = 0; //debug code for differential readings per interval
    interrupts();
    //Serial.print("Encoder Ticks: ");//remove to minimize serial
    Serial.print(readEncoderTicks);
    Serial.print("\n");
    dac.setVoltage(readEncoderTicks, false);
  }
}

void HandleMotorInterrupt() {
  encState = ((encState << 2) | ((PIND >> 2) & 3)) & 15; //use encoder bits and last state to form index, mask 0x0F
  EncoderTicks += encTable[encState];//update actual position on encoder movement
  if (EncoderTicks >= 4096)
  {
    EncoderTicks = 0;
  }

}

Sorry for the delayed response. I'd have to agree that your modified code is definitely the fastest so far. Even with your code, I was still able to make the system freeze when I spun the encoder quickly. So, we wanted to try using a lower pulses-per-rev encoder (250 PPR) to see if that would fix the issue. Between the new encoder, your code, and a shorter readInterval (10 ms - 100 Hz) I can run the program without it freezing. There are some other wrinkles that we are trying to iron out, but this is a real start to what we are looking for. Thanks for your help so far. I'll post here if I have other questions.

Possibly an absolute encoder would suit.

I'm glad to hear that you are making progress. Whether or not you can make the analog output through the i2c dac work well will depend on the resolution you require for angular position and the rotational speed.

Using the lower resolution encoder will be similar to reading the original one with an algorithm which does not read all the quadrature states. Encoder reading algorithms can give transitional counts equal to 1x, 2x, or 4x the specified counts per revolution, depending on which interrupts are used.

The algorithm you are currently using can be made slightly faster by using pin change interrupts instead of the external interrupt with attachInterrupt(). It can also be modified to give 1024 or 2048 readings per revolution instead of the 4096. If you want assistance with modifiying the routine for the original encoder let me know.

You have also never really spelled out your requirements other than that 400rpm was too fast.

I believe that with the fast encoder reading application you will be able to do much more with digital output. You have never explained what is using the the 0-5v output, and what are the barriers to using the encoder counts directly? I'm sorry to keep bugging you on this point, but I am convinced that if a digital approach is possible, it is the path to better performance.

What is the application? What are the requirements? What angular resolution is required at what rotational speeds?

When you are using an incremental encoder instead of an absolute encoder for angular position, there is the issue of a home position or 0 reading. How are you dealing with this?